Ручной ввод: без обязательных галок, удаление черновика, запрет количества больше остатка; отказ НРД по поручению — красным
- ручных отметок перед отправкой нет: автор виден по учётной записи, клиент/счёт/остаток/
бумага проверены сверкой с DEPO при сохранении; блокировка в QUIK — необязательно,
по умолчанию не требуется (модуль неторговых поручений сейчас не используется)
- «Удалить черновик» в карточке (POST /admin/manual/{id}/delete), только для черновиков
- количество не больше остатка на торговом разделе: проверка в форме (по выбранной бумаге)
и на сервере при сохранении (по счёту DEPO) — поручение с превышением не сохраняется
- отчёт MS010 с отказом: orderResponse.Rejected, OutgoingDeal.Rejected; шаг «Операция
завершена отказом» и бейдж — красные в карточках исходящего и входящего; онлайн-статус
«поручение 16 завершено отказом НРД»
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -165,20 +165,21 @@ func registerIncoming(mux *http.ServeMux, a *admin, deps incomingDeps) {
|
||||
return
|
||||
}
|
||||
var orderResps []orderResponse
|
||||
var orderDone bool
|
||||
var orderDone, orderRejected bool
|
||||
var orderDoneAt string
|
||||
if it.OrderSubmitted {
|
||||
orderResps = findOrderResponses(it.OrderContrDID)
|
||||
for _, rp := range orderResps {
|
||||
if rp.Done { // отчёт MS010 — операция проведена
|
||||
if rp.Done { // отчёт MS010 — операция проведена или завершена отказом
|
||||
orderDone = true
|
||||
orderDoneAt = rp.Time
|
||||
orderRejected = orderRejected || rp.Rejected
|
||||
}
|
||||
}
|
||||
}
|
||||
rejected, codes := incomingRejectedAll(it)
|
||||
data := incomingDetailData{page: nowPage("Входящий перевод", "incoming"), It: it, Flash: r.URL.Query().Get("flash"),
|
||||
OrderResponses: orderResps, OrderDone: orderDone, OrderDoneAt: orderDoneAt, DraftReceived: draftReceivedTime(it.GUID),
|
||||
OrderResponses: orderResps, OrderDone: orderDone, OrderRejected: orderRejected, OrderDoneAt: orderDoneAt, DraftReceived: draftReceivedTime(it.GUID),
|
||||
Quik: globalQuikLinks.ForGUID(it.GUID), QuikOn: quikActive(deps.rc.Snapshot().Quik), Watch: globalOrderWatch.Get(it.GUID), DepoL: globalDepoLinks.Get(it.GUID),
|
||||
Rejected: rejected, RejectCodes: codes, AutoMode: deps.rc.Snapshot().M2MAuto.Enabled}
|
||||
// Сверка выполняется при открытии карточки; итог сохраняем для бейджа в списке.
|
||||
@@ -236,6 +237,7 @@ type incomingDetailData struct {
|
||||
Flash string
|
||||
OrderResponses []orderResponse
|
||||
OrderDone bool
|
||||
OrderRejected bool // MS010 с отказом
|
||||
OrderDoneAt string
|
||||
DraftReceived string
|
||||
Depo depoResult // сверка с DEPO — рекомендация по ответу, ничего не отправляется
|
||||
|
||||
@@ -198,7 +198,9 @@ type orderResponse struct {
|
||||
Kind string // человекочитаемый тип (квитанция / принятие / отчёт MS010)
|
||||
Time string
|
||||
Text string // краткое содержимое
|
||||
Done bool // true — это отчёт MS010 (операция проведена)
|
||||
Done bool // true — это отчёт MS010 (операция проведена или завершена отказом)
|
||||
// Rejected — MS010 с отказом: операция завершилась, но не исполнена. Показывается красным.
|
||||
Rejected bool
|
||||
}
|
||||
|
||||
// findOrderResponses ищет в INBOX ИШ ответы НРД по нашему поручению (по contr_d_id).
|
||||
@@ -222,7 +224,7 @@ func findOrderResponses(contrDID string) []orderResponse {
|
||||
}
|
||||
kind, done := orderRespKind(dir, body)
|
||||
tmp = append(tmp, rr{orderResponse{
|
||||
Pkg: dir, Kind: kind, Done: done,
|
||||
Pkg: dir, Kind: kind, Done: done, Rejected: done && respTextRejected(body),
|
||||
Time: mt.In(time.FixedZone("МСК", 3*3600)).Format("02.01.2006 15:04:05"),
|
||||
Text: orderRespSummary(body),
|
||||
}, mt})
|
||||
@@ -453,3 +455,9 @@ func orderRespSummary(body string) string {
|
||||
}
|
||||
return txt
|
||||
}
|
||||
|
||||
// respTextRejected — в отчёте НРД сказано об отказе, а не об исполнении.
|
||||
func respTextRejected(body string) bool {
|
||||
low := strings.ToLower(body)
|
||||
return strings.Contains(low, "отказ") || strings.Contains(low, "reject") || strings.Contains(low, "не исполнен")
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ func liveOutgoing(c ClaimView) LiveStatus {
|
||||
cp := *d
|
||||
if cp.Submitted && len(cp.Responses) == 0 && !cp.Done {
|
||||
cp.Responses, cp.Done = outgoingResponses(cp.ContrDIDs) // ответы ищем только для этой, живой сделки
|
||||
_, cp.Rejected, _ = orderRespVerdict(cp.Responses)
|
||||
}
|
||||
depo = &cp
|
||||
}
|
||||
@@ -89,6 +90,8 @@ func liveOutgoing(c ClaimView) LiveStatus {
|
||||
return LiveStatus{Text: "принята контрагентом, ожидает черновика поручения 16 от НРД", Kind: "wait", Active: true}
|
||||
case !depo.Submitted:
|
||||
return LiveStatus{Text: "черновик поручения 16 получен, ожидает подачи в НРД", Kind: "warn", Active: true}
|
||||
case depo.Done && depo.Rejected:
|
||||
return LiveStatus{Text: "поручение 16 завершено отказом НРД (MS010)", Kind: "bad"}
|
||||
case depo.Done:
|
||||
return LiveStatus{Text: "поручение 16 исполнено: MS010 получен", Kind: "ok"}
|
||||
case watch != nil:
|
||||
|
||||
@@ -266,6 +266,21 @@ func (s *manualStore) Create(o *ManualOrder) error {
|
||||
}
|
||||
|
||||
// Update — изменение под замком: fn получает текущую запись.
|
||||
// Delete — удалить черновик. Отправленные и отклонённые поручения не удаляются: они след.
|
||||
func (s *manualStore) Delete(id int) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
o, ok := s.items[id]
|
||||
if !ok {
|
||||
return fmt.Errorf("поручение не найдено")
|
||||
}
|
||||
if o.Status != "draft" {
|
||||
return fmt.Errorf("удалить можно только черновик, а поручение уже %s", manualStatusRu(o.Status))
|
||||
}
|
||||
delete(s.items, id)
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
func (s *manualStore) Update(id int, fn func(o *ManualOrder) error) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -382,11 +397,8 @@ func manualSendBlockers(rc *RuntimeConfig, o *ManualOrder) []string {
|
||||
b = append(b, "сверка с DEPO не пройдена: "+o.Depo.Summary)
|
||||
}
|
||||
}
|
||||
for _, c := range manualPreChecks {
|
||||
if !o.Checks[c.Key].Done {
|
||||
b = append(b, "нет отметки: "+c.Title)
|
||||
}
|
||||
}
|
||||
// Ручных отметок перед отправкой нет: кто создал поручение — видно по учётной записи,
|
||||
// а клиент, счёт, остаток и бумага проверены сверкой с учётной системой при сохранении.
|
||||
switch o.QuikMode {
|
||||
case "auto":
|
||||
if !manualQuikAutoAvailable(rc) {
|
||||
@@ -396,9 +408,8 @@ func manualSendBlockers(rc *RuntimeConfig, o *ManualOrder) []string {
|
||||
if strings.TrimSpace(o.QuikInstr) == "" {
|
||||
b = append(b, "укажите номер поручения на блокировку в торговой системе")
|
||||
}
|
||||
case "none":
|
||||
default:
|
||||
b = append(b, "не выбрано, как заблокированы бумаги в торговой системе")
|
||||
case "none", "":
|
||||
// модуль неторговых поручений QUIK не используется — блокировка не требуется
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package lkgateway
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -195,3 +196,62 @@ func registerManualDepo(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
reply(w, items, errS)
|
||||
})
|
||||
}
|
||||
|
||||
// manualDepoClientByAccount — OD_CLIENTS.ID открытого торгового счёта по его номеру.
|
||||
func manualDepoClientByAccount(rc *RuntimeConfig, number string) (int64, string) {
|
||||
number = strings.TrimSpace(number)
|
||||
if number == "" || !manualDepoConfigured(rc) {
|
||||
return 0, ""
|
||||
}
|
||||
db, ctx, cancel, errS := depoOpen(rc.Snapshot().Fancy)
|
||||
if errS != "" {
|
||||
return 0, errS
|
||||
}
|
||||
defer cancel()
|
||||
defer db.Close()
|
||||
var id int64
|
||||
err := db.QueryRowContext(ctx, `SELECT TOP 1 ID FROM OD_CLIENTS WHERE NUMBER=@n AND STATE=0 AND D_CLOSE IS NULL ORDER BY ID`,
|
||||
sql.Named("n", number)).Scan(&id)
|
||||
if err != nil {
|
||||
return 0, ""
|
||||
}
|
||||
return id, ""
|
||||
}
|
||||
|
||||
// manualQtyOverAvailable — количество в поручении больше остатка на торговом разделе.
|
||||
// Пусто — всё в порядке или остаток проверить нельзя (DEPO не подключена, счёт не найден):
|
||||
// тогда решает сверка при сохранении. Смысл: поручение на 111 бумаг при 110 на счёте не
|
||||
// должно даже сохраниться — дальше по цепочке ему всё равно идти некуда.
|
||||
func manualQtyOverAvailable(rc *RuntimeConfig, depoClient string, o *ManualOrder) string {
|
||||
if !manualDepoConfigured(rc) || o == nil {
|
||||
return ""
|
||||
}
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(depoClient), 10, 64)
|
||||
if id <= 0 {
|
||||
id, _ = manualDepoClientByAccount(rc, o.Account)
|
||||
}
|
||||
if id <= 0 {
|
||||
return ""
|
||||
}
|
||||
pos, errS := manualDepoPositions(rc, id)
|
||||
if errS != "" {
|
||||
return ""
|
||||
}
|
||||
for _, sec := range o.Securities {
|
||||
isin := strings.ToUpper(strings.TrimSpace(sec.ISIN))
|
||||
if isin == "" {
|
||||
continue
|
||||
}
|
||||
q, err := strconv.ParseFloat(strings.ReplaceAll(strings.TrimSpace(sec.Qty), ",", "."), 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, p := range pos {
|
||||
if p.ISIN == isin && q > p.Qty+1e-9 {
|
||||
return fmt.Sprintf("По бумаге %s на торговом разделе счёта %s доступно %s шт., а в поручении %s — сохранить нельзя: укажите не больше доступного.",
|
||||
isin, o.Account, strconv.FormatFloat(p.Qty, 'f', -1, 64), strings.TrimSpace(sec.Qty))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -127,6 +127,16 @@ func registerManual(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
|
||||
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||
actor := manualActor(w)
|
||||
msg := "Поручение сохранено"
|
||||
if over := manualQtyOverAvailable(rc, r.FormValue("depo_client"), in); over != "" {
|
||||
if id == 0 {
|
||||
d := base("Новое поручение")
|
||||
d.Mode, d.O, d.Handbook, d.Flash = "edit", in, handbookParticipants(), over
|
||||
render(w, tpl, d)
|
||||
return
|
||||
}
|
||||
back(w, r, fmt.Sprintf("/admin/manual/%d", id), over, "#mf")
|
||||
return
|
||||
}
|
||||
if id == 0 {
|
||||
in.Status, in.CreatedBy = "draft", actor
|
||||
if err := globalManual.Create(in); err != nil {
|
||||
@@ -265,6 +275,13 @@ func registerManual(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
|
||||
})
|
||||
log.Printf("lk-gateway: ручной ввод %s (%s): отправка — %s", o.Number(), o.FullName(), msg)
|
||||
back(w, r, card, msg, "")
|
||||
case "delete":
|
||||
if err := globalManual.Delete(id); err != nil {
|
||||
back(w, r, card, err.Error(), "")
|
||||
return
|
||||
}
|
||||
back(w, r, "/admin/manual", fmt.Sprintf("Черновик %d удалён", id), "")
|
||||
return
|
||||
case "decline":
|
||||
reason := strings.TrimSpace(r.FormValue("reason"))
|
||||
err := globalManual.Update(id, func(o *ManualOrder) error {
|
||||
|
||||
@@ -25,19 +25,16 @@ func TestManualSendBlockers(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
o := manualTestOrder()
|
||||
b := manualSendBlockers(rc, o)
|
||||
if len(b) != len(manualPreChecks)+1 { // все отметки + способ блокировки в QUIK
|
||||
t.Fatalf("без отметок: %v", b)
|
||||
}
|
||||
o.Checks = map[string]ManualMark{}
|
||||
for _, c := range manualPreChecks {
|
||||
o.Checks[c.Key] = ManualMark{Done: true}
|
||||
// Ручных отметок нет, блокировка в QUIK по умолчанию не требуется: черновик готов сразу.
|
||||
if b := manualSendBlockers(rc, o); len(b) != 0 {
|
||||
t.Fatalf("готовый черновик: %v", b)
|
||||
}
|
||||
o.QuikMode = "manual"
|
||||
if b = manualSendBlockers(rc, o); len(b) != 1 || !strings.Contains(b[0], "номер поручения") {
|
||||
if b := manualSendBlockers(rc, o); len(b) != 1 || !strings.Contains(b[0], "номер поручения") {
|
||||
t.Fatalf("ручная блокировка без номера: %v", b)
|
||||
}
|
||||
o.QuikInstr = "123"
|
||||
var b []string
|
||||
if b = manualSendBlockers(rc, o); len(b) != 0 {
|
||||
t.Fatalf("всё заполнено: %v", b)
|
||||
}
|
||||
@@ -113,19 +110,16 @@ func TestManualPages(t *testing.T) {
|
||||
if o.DocSeries != "0000" || o.Account != "СЧЁТ-1" || len(o.Securities) != 1 || o.Securities[0].ISIN != "RU0009029540" {
|
||||
t.Fatalf("данные: %+v", o)
|
||||
}
|
||||
if rec = do("GET", "/admin/manual/1", nil); rec.Code != 200 || !strings.Contains(rec.Body.String(), "Отправить пока нельзя") {
|
||||
if rec = do("GET", "/admin/manual/1", nil); rec.Code != 200 || !strings.Contains(rec.Body.String(), "Сверка пройдена, поручение готово") {
|
||||
t.Fatalf("карточка: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
chk := url.Values{"quik_mode": {"none"}}
|
||||
for _, c := range manualPreChecks {
|
||||
chk.Set("check_"+c.Key, "1")
|
||||
}
|
||||
chk := url.Values{"quik_mode": {"manual"}, "quik_instr": {"77"}}
|
||||
do("POST", "/admin/manual/1/checks", chk)
|
||||
o, _ = globalManual.Get(1)
|
||||
if len(manualSendBlockers(rc, o)) != 0 || o.Checks["paper"].At.IsZero() {
|
||||
t.Fatalf("отметки: %+v %v", o.Checks, manualSendBlockers(rc, o))
|
||||
if len(manualSendBlockers(rc, o)) != 0 || o.QuikMode != "manual" {
|
||||
t.Fatalf("блокировка вручную: %+v %v", o.QuikMode, manualSendBlockers(rc, o))
|
||||
}
|
||||
if rec = do("GET", "/admin/manual/1", nil); !strings.Contains(rec.Body.String(), "Все проверки и отметки пройдены") {
|
||||
if rec = do("GET", "/admin/manual/1", nil); !strings.Contains(rec.Body.String(), "Сверка пройдена, поручение готово") {
|
||||
t.Fatalf("готово к отправке: %s", rec.Body.String())
|
||||
}
|
||||
// Изменение данных сбрасывает отметки.
|
||||
@@ -135,6 +129,15 @@ func TestManualPages(t *testing.T) {
|
||||
if o, _ = globalManual.Get(1); len(o.Checks) != 0 {
|
||||
t.Fatalf("отметки должны сброситься: %+v", o.Checks)
|
||||
}
|
||||
// Черновик можно удалить; отклонённое — нет.
|
||||
do("POST", "/admin/manual/save", url.Values{"paper_date": {"2026-09-17"}, "last_name": {"Пётр"}, "first_name": {"П"}, "birth_date": {"1980-01-02"},
|
||||
"doc_type": {"21"}, "doc_series": {"0000"}, "doc_number": {"000001"}, "account": {"счёт-2"}, "recv_inn": {"7702165310"}, "sec_isin": {"ru0009029540"}, "sec_qty": {"1"}, "do": {"save"}})
|
||||
if rec = do("POST", "/admin/manual/2/delete", nil); rec.Code != http.StatusSeeOther || !strings.Contains(rec.Header().Get("Location"), "/admin/manual?") {
|
||||
t.Fatalf("удаление черновика: %d %s", rec.Code, rec.Header().Get("Location"))
|
||||
}
|
||||
if _, ok := globalManual.Get(2); ok {
|
||||
t.Fatal("черновик не удалён")
|
||||
}
|
||||
do("POST", "/admin/manual/1/decline", url.Values{"reason": {"нет подписи клиента"}})
|
||||
o, _ = globalManual.Get(1)
|
||||
if o.Status != "declined" || o.DeclinedAt.Before(time.Now().Add(-time.Minute)) {
|
||||
|
||||
@@ -148,6 +148,7 @@ type OutgoingDeal struct {
|
||||
// ответ НРД:
|
||||
Responses []orderResponse
|
||||
Done bool // получен MS010 по всем поручениям
|
||||
Rejected bool // MS010 с отказом: операция завершена, но не исполнена
|
||||
}
|
||||
|
||||
// scanForm16Drafts живьём читает INBOX и собирает исходящие сделки: все черновики
|
||||
@@ -247,6 +248,7 @@ func scanForm16DraftsOpt(store *OutgoingStore, withResponses bool) []*OutgoingDe
|
||||
d.ContrDIDs = sub.ContrDIDs
|
||||
if withResponses {
|
||||
d.Responses, d.Done = outgoingResponses(sub.ContrDIDs)
|
||||
_, d.Rejected, _ = orderRespVerdict(d.Responses)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,12 +162,17 @@
|
||||
.ord-step.wait .dot{background:#f59e0b;color:#fff;box-shadow:0 0 0 4px color-mix(in srgb,#f59e0b 28%,transparent)}
|
||||
.ord-step.wait .s{color:#b45309}
|
||||
.ord-step.todo{opacity:.55}
|
||||
.ord-step.bad{border-color:#dc2626;background:color-mix(in srgb,#dc2626 12%,var(--card))}
|
||||
.ord-step.bad .dot{background:#dc2626;color:#fff}
|
||||
.ord-step.bad .s{color:#b3261e}
|
||||
.ord-dl.bad{border-color:#dc2626;color:#b3261e}
|
||||
.ord-dl{display:inline-block;margin-top:9px;font-size:11.5px;font-weight:700;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid #16a34a;color:#0f7a37;text-decoration:none;white-space:nowrap}
|
||||
.ord-dl:hover{background:color-mix(in srgb,#16a34a 12%,var(--card))}
|
||||
</style>
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<h2 style="margin:0">Депозитарное поручение 16 (списание)</h2>
|
||||
{{if .Depo.Done}}<span class="badge confirmed">проведено · MS010</span>
|
||||
{{if and .Depo.Done .Depo.Rejected}}<span class="badge rejected">завершено отказом · MS010</span>
|
||||
{{else if .Depo.Done}}<span class="badge confirmed">проведено · MS010</span>
|
||||
{{else if .Depo.Submitted}}<span class="badge awaiting_decision">подано, ждём MS010</span>
|
||||
{{else}}<span class="badge" style="background:var(--card-2);color:var(--muted)">требует подачи</span>{{end}}
|
||||
</div>
|
||||
@@ -186,10 +191,10 @@
|
||||
<div><div class="t">Поручение 16 подано</div><div class="s">{{if .Depo.Submitted}}{{msk .Depo.OrderAt}}{{else}}нужно подать в НРД{{end}}</div>
|
||||
{{if .Depo.OrderXML}}<a class="ord-dl" href="/admin/outgoing/{{.Depo.GUID}}/order.xml">⬇ Отправленная форма 16</a>{{end}}</div>
|
||||
</div>
|
||||
<div class="ord-step {{if .Depo.Done}}done{{else if .Depo.Submitted}}wait{{else}}todo{{end}}">
|
||||
<span class="dot">{{if .Depo.Done}}✓{{else}}3{{end}}</span>
|
||||
<div><div class="t">Списание проведено · MS010</div><div class="s">{{if .Depo.Done}}отчёт получен от НРД{{else if .Depo.Submitted}}ждём встречный 16/1 + MS010{{else}}после подачи{{end}}</div>
|
||||
{{if .Depo.Responses}}<a class="ord-dl" href="/admin/outgoing/{{.Depo.GUID}}/ms010.zip">⬇ Ответ НРД (MS010)</a>{{end}}</div>
|
||||
<div class="ord-step {{if and .Depo.Done .Depo.Rejected}}bad{{else if .Depo.Done}}done{{else if .Depo.Submitted}}wait{{else}}todo{{end}}">
|
||||
<span class="dot">{{if and .Depo.Done .Depo.Rejected}}✕{{else if .Depo.Done}}✓{{else}}3{{end}}</span>
|
||||
<div><div class="t">{{if and .Depo.Done .Depo.Rejected}}Операция завершена отказом · MS010{{else}}Списание проведено · MS010{{end}}</div><div class="s">{{if and .Depo.Done .Depo.Rejected}}НРД отказал в исполнении — см. отчёт{{else if .Depo.Done}}отчёт получен от НРД{{else if .Depo.Submitted}}ждём встречный 16/1 + MS010{{else}}после подачи{{end}}</div>
|
||||
{{if .Depo.Responses}}<a class="ord-dl{{if .Depo.Rejected}} bad{{end}}" href="/admin/outgoing/{{.Depo.GUID}}/ms010.zip">⬇ Ответ НРД (MS010)</a>{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -226,6 +226,10 @@
|
||||
.ord-step.wait .dot{background:#f59e0b;color:#fff;box-shadow:0 0 0 4px color-mix(in srgb,#f59e0b 28%,transparent)}
|
||||
.ord-step.wait .s{color:#b45309}
|
||||
.ord-step.todo{opacity:.55}
|
||||
.ord-step.bad{border-color:#dc2626;background:color-mix(in srgb,#dc2626 12%,var(--card))}
|
||||
.ord-step.bad .dot{background:#dc2626;color:#fff}
|
||||
.ord-step.bad .s{color:#b3261e}
|
||||
.ord-dl.bad{border-color:#dc2626;color:#b3261e}
|
||||
.ord-dl{display:inline-block;margin-top:9px;font-size:11.5px;font-weight:700;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid #16a34a;color:#0f7a37;text-decoration:none;white-space:nowrap}
|
||||
.ord-dl:hover{background:color-mix(in srgb,#16a34a 12%,var(--card))}
|
||||
</style>
|
||||
@@ -265,9 +269,9 @@
|
||||
<div><div class="t">Поручение подано в НРД</div><div class="s">{{if .It.OrderSubmitted}}{{msk .It.OrderAt}}{{else if .AutoMode}}bj подаст автоматически по черновику{{else if .DraftReceived}}нужно подать{{else}}после черновика{{end}}</div>
|
||||
{{if .It.OrderXML}}<a class="ord-dl" href="/admin/incoming/{{.It.ID}}/order.xml">⬇ Отправленная форма 16/1</a>{{end}}</div>
|
||||
</div>
|
||||
<div class="ord-step {{if .OrderDone}}done{{else if .It.OrderSubmitted}}wait{{else}}todo{{end}}">
|
||||
<span class="dot">{{if .OrderDone}}✓{{else}}3{{end}}</span>
|
||||
<div><div class="t">Проведено · MS010</div><div class="s">{{if .OrderDone}}{{if .OrderDoneAt}}{{.OrderDoneAt}}{{else}}отчёт получен от НРД{{end}}{{else if .It.OrderSubmitted}}ждём MS010 от НРД{{else}}после подачи{{end}}</div>
|
||||
<div class="ord-step {{if and .OrderDone .OrderRejected}}bad{{else if .OrderDone}}done{{else if .It.OrderSubmitted}}wait{{else}}todo{{end}}">
|
||||
<span class="dot">{{if and .OrderDone .OrderRejected}}✕{{else if .OrderDone}}✓{{else}}3{{end}}</span>
|
||||
<div><div class="t">{{if and .OrderDone .OrderRejected}}Операция завершена отказом · MS010{{else}}Проведено · MS010{{end}}</div><div class="s">{{if and .OrderDone .OrderRejected}}НРД отказал в исполнении{{if .OrderDoneAt}} · {{.OrderDoneAt}}{{end}}{{else if .OrderDone}}{{if .OrderDoneAt}}{{.OrderDoneAt}}{{else}}отчёт получен от НРД{{end}}{{else if .It.OrderSubmitted}}ждём MS010 от НРД{{else}}после подачи{{end}}</div>
|
||||
{{if .OrderResponses}}<a class="ord-dl" href="/admin/incoming/{{.It.ID}}/ms010.zip">⬇ Ответ НРД (MS010)</a>{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -286,7 +290,7 @@
|
||||
<table style="margin-top:8px">
|
||||
<thead><tr><th style="width:150px">Время</th><th>Ответ НРД</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .OrderResponses}}<tr><td style="white-space:nowrap">{{.Time}}</td><td>{{.Kind}}</td></tr>{{end}}
|
||||
{{range .OrderResponses}}<tr><td style="white-space:nowrap">{{.Time}}</td><td{{if .Rejected}} style="color:#b3261e;font-weight:600"{{end}}>{{.Kind}}</td></tr>{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
@@ -349,7 +353,7 @@
|
||||
<div class="st st-nrd">
|
||||
<div class="tm">{{.Time}}</div>
|
||||
<div class="ti">Ответ НРД</div>
|
||||
<div class="de">{{.Kind}}</div>
|
||||
<div class="de"{{if .Rejected}} style="color:#b3261e;font-weight:600"{{end}}>{{.Kind}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</aside>
|
||||
|
||||
@@ -176,19 +176,13 @@
|
||||
<div class="card"><h2>Сверка с учётной системой</h2><p class="muted" style="margin:0">Учётная система депозитария не подключена к bj — сверку выполните вручную и подтвердите отметками ниже.</p></div>
|
||||
{{end}}
|
||||
|
||||
{{/* ---- Обязательные отметки ---- */}}
|
||||
{{if or (eq $o.Status "draft") $o.Checks}}
|
||||
{{/* ---- Торговая система: блокировка (необязательно) ---- */}}
|
||||
{{if or (eq $o.Status "draft") $o.QuikMode}}
|
||||
<div class="card" id="checks">
|
||||
<h2>Обязательные отметки перед отправкой</h2>
|
||||
<h2>Блокировка в торговой системе <span class="muted" style="font-size:13px;font-weight:500">· необязательно</span></h2>
|
||||
<p class="muted" style="margin:0 0 8px">Клиент, счёт, остаток и бумага проверяются сверкой с учётной системой при сохранении — отдельных отметок не нужно. Здесь только отметьте, если бумаги заблокированы в торговой системе; по умолчанию блокировка не требуется.</p>
|
||||
{{$editable := and (eq $o.Status "draft") (eq .Mode "card") (can "operate")}}
|
||||
<form method="post" action="/admin/manual/{{$o.ID}}/checks" id="chkForm">
|
||||
{{$grp := ""}}
|
||||
{{range .PreChecks}}
|
||||
{{if ne .Group $grp}}<div class="mf-grp">{{.Group}}</div>{{$grp = .Group}}{{end}}
|
||||
{{$m := index $o.Checks .Key}}
|
||||
<label class="mf-chk"><input type="checkbox" name="check_{{.Key}}" value="1" {{if $m.Done}}checked{{end}} {{if not $editable}}disabled{{end}} onchange="this.form.submit()">
|
||||
<span><div class="t">{{.Title}}</div><div class="h">{{.Hint}}</div>{{if $m.Done}}<div class="who">✓ {{$m.By}} · {{msk $m.At}}</div>{{end}}</span></label>
|
||||
{{end}}
|
||||
<div class="mf-grp">Торговая система (QUIK)</div>
|
||||
<label class="mf-chk"><input type="radio" name="quik_mode" value="auto" {{if eq $o.QuikMode "auto"}}checked{{end}} {{if or (not $editable) (not .QuikAuto)}}disabled{{end}} onchange="this.form.submit()">
|
||||
<span><div class="t">Заблокировать бумаги автоматически при отправке</div><div class="h">{{if .QuikAuto}}bj зарегистрирует в модуле неторговых поручений вывод бумаг (лимит уменьшится), по исполнению подпишет списание, по отказу снимет блокировку.{{else}}Недоступно: автоматический цикл в торговой системе не включён в настройках.{{end}}</div></span></label>
|
||||
@@ -196,7 +190,7 @@
|
||||
<span><div class="t">Бумаги заблокированы вручную (лимит уменьшен)</div><div class="h">Позиция клиента проверена, поручение на вывод зарегистрировано сотрудником. После ответа отметьте списание или снятие блокировки.</div>
|
||||
{{if eq $o.QuikMode "manual"}}<div style="margin-top:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap"><span class="mf-note">№ поручения в торговой системе:</span><input name="quik_instr" value="{{$o.QuikInstr}}" {{if not $editable}}disabled{{end}} style="max-width:200px;padding:5px 8px;border-radius:7px;border:1px solid var(--border);background:var(--card);color:var(--text)">{{if $editable}}<button type="submit" class="btn btn-secondary" style="padding:5px 10px">Сохранить</button>{{end}}</div>{{end}}
|
||||
</span></label>
|
||||
<label class="mf-chk"><input type="radio" name="quik_mode" value="none" {{if eq $o.QuikMode "none"}}checked{{end}} {{if not $editable}}disabled{{end}} onchange="this.form.submit()">
|
||||
<label class="mf-chk"><input type="radio" name="quik_mode" value="none" {{if or (eq $o.QuikMode "none") (eq $o.QuikMode "")}}checked{{end}} {{if not $editable}}disabled{{end}} onchange="this.form.submit()">
|
||||
<span><div class="t">Блокировка в торговой системе не требуется</div><div class="h">Бумаги не учитываются в торговой системе (не на торговом разделе, клиент не пользуется брокерским обслуживанием).</div></span></label>
|
||||
{{if and $editable (ne $o.QuikMode "manual")}}<input type="hidden" name="quik_instr" value="{{$o.QuikInstr}}">{{end}}
|
||||
</form>
|
||||
@@ -211,13 +205,16 @@
|
||||
<p class="muted" style="margin:0">Отправить пока нельзя:</p>
|
||||
<ul class="mf-block">{{range .Blockers}}<li>{{.}}</li>{{end}}</ul>
|
||||
{{else}}
|
||||
<p style="margin:0"><span class="dc-ok">Все проверки и отметки пройдены.</span> Поручение уйдёт в НРД тем же путём, что и заявка из личного кабинета{{if eq $o.QuikMode "auto"}}, бумаги будут заблокированы в торговой системе перед отправкой{{end}}.</p>
|
||||
<p style="margin:0"><span class="dc-ok">Сверка пройдена, поручение готово.</span> Поручение уйдёт в НРД тем же путём, что и заявка из личного кабинета{{if eq $o.QuikMode "auto"}}, бумаги будут заблокированы в торговой системе перед отправкой{{end}}.</p>
|
||||
{{end}}
|
||||
{{if can "operate"}}
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:14px;align-items:flex-start">
|
||||
<form method="post" action="/admin/manual/{{$o.ID}}/send" onsubmit="return confirm('Отправить поручение {{$o.Number}} ({{$o.FullName}}) в НРД? Отменить отправку будет нельзя.')">
|
||||
<button type="submit" class="btn btn-ok" {{if .Blockers}}disabled style="opacity:.5;cursor:not-allowed"{{end}}>Отправить в НРД</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/manual/{{$o.ID}}/delete" style="margin:0" onsubmit="return confirm('Удалить черновик {{$o.Number}}? Восстановить его будет нельзя.')">
|
||||
<button type="submit" class="btn btn-secondary">Удалить черновик</button>
|
||||
</form>
|
||||
<details><summary class="btn btn-secondary" style="list-style:none;cursor:pointer">Отклонить поручение…</summary>
|
||||
<form method="post" action="/admin/manual/{{$o.ID}}/decline" style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap" onsubmit="return confirm('Отклонить поручение без отправки в НРД?')">
|
||||
<input name="reason" required placeholder="причина отказа клиенту" style="min-width:320px;padding:7px 9px;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text)">
|
||||
@@ -296,8 +293,8 @@ function mfRerun(){const f=document.getElementById('mf');if(f&&f.closest('detail
|
||||
|
||||
{{/* ======================= ФОРМА (новое / черновик) ======================= */}}
|
||||
{{define "manualForm"}}
|
||||
<form method="post" action="/admin/manual/save" id="mf">
|
||||
<input type="hidden" name="id" value="{{.O.ID}}">
|
||||
<form method="post" action="/admin/manual/save" id="mf" onsubmit="return mfQtyOK()">
|
||||
<input type="hidden" name="id" value="{{.O.ID}}"><input type="hidden" name="depo_client" id="f_depo_client" value="">
|
||||
<div class="mf-h" style="margin-top:0">1. Основание</div>
|
||||
<div class="mf-grid">
|
||||
<div><label class="mf-req">Дата поручения</label><input type="date" name="paper_date" value="{{.O.PaperDate}}" required></div>
|
||||
@@ -384,6 +381,7 @@ async function mfPick(i,j){
|
||||
if(f.doc_type)mfSet('doc_type',f.doc_type);
|
||||
mfSet('doc_series',f.doc_ser||'');mfSet('doc_number',f.doc_num);mfSet('doc_issue_date',f.doc_date||'');mfSet('doc_issuer',f.doc_issuer||'');
|
||||
mfSet('account',a.number);mfSet('contract',a.contract||'');if(f.phone)mfSet('phone',f.phone);
|
||||
const dc=document.getElementById('f_depo_client');if(dc)dc.value=a.client_id||'';
|
||||
document.getElementById('dres').innerHTML='<span class="dc-ok">Подставлено из DEPO: '+mfEsc(f.name)+', счёт '+mfEsc(a.number)+'. Сверьте с поручением клиента.</span>';
|
||||
const box=document.getElementById('dpos');box.innerHTML='<span class="mf-note">Загрузка бумаг клиента…</span>';
|
||||
try{const r=await (await fetch('/admin/manual/depo/positions?client='+a.client_id)).json();
|
||||
@@ -394,7 +392,11 @@ async function mfPick(i,j){
|
||||
}catch(e){box.innerHTML='<span class="dc-bad">'+mfEsc(e)+'</span>'}
|
||||
}
|
||||
let mfPos=[];
|
||||
function mfSec(k){const p=mfPos[k];document.getElementById('f_isin').value=p.isin;document.getElementById('f_sname').value=p.name;const q=document.getElementById('f_qty');q.placeholder='доступно '+p.qty;q.focus()}
|
||||
function mfSec(k){const p=mfPos[k];document.getElementById('f_isin').value=p.isin;document.getElementById('f_sname').value=p.name;const q=document.getElementById('f_qty');q.placeholder='доступно '+p.qty;q.dataset.avail=p.qty;q.dataset.isin=p.isin;q.focus()}
|
||||
// Больше остатка на торговом разделе ввести нельзя: такому поручению дальше идти некуда.
|
||||
function mfQtyOK(){const q=document.getElementById('f_qty'),isin=document.getElementById('f_isin');if(!q||!q.dataset.avail||(isin&&isin.value.toUpperCase()!==q.dataset.isin))return true;
|
||||
const v=parseFloat(String(q.value).replace(',','.'));if(isNaN(v))return true;
|
||||
if(v>parseFloat(q.dataset.avail)){alert('По бумаге '+q.dataset.isin+' доступно '+q.dataset.avail+' шт., в поручении '+q.value+'. Укажите не больше доступного.');q.focus();return false}return true}
|
||||
// hbFind — подбор участника справочника НРД по ИНН, названию или коду (без учёта регистра и кавычек).
|
||||
function hbNorm(s){return String(s||'').toLowerCase().replace(/ё/g,'е').replace(/[«»"'“”.,()]/g,' ').replace(/\s+/g,' ').trim()}
|
||||
function hbFind(list,q){q=hbNorm(q);
|
||||
|
||||
Reference in New Issue
Block a user