Справочник участников: поиск по ИНН/названию/коду вместо списка (ручной ввод, организация); ручной ввод — только НРД

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-17 20:02:42 +03:00
parent 634244ee28
commit a9dc14969b
5 changed files with 65 additions and 18 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ type ManualOrder struct {
RecvCode string `json:"recv_code,omitempty"`
RecvName string `json:"recv_name,omitempty"`
RecvAccount string `json:"recv_account,omitempty"` // счёт клиента у принимающего (для сведения)
SettlePlace string `json:"settle_place"` // nrd | spb
SettlePlace string `json:"settle_place"` // nrd (СПБ Банк пока не используется)
CostInfo bool `json:"cost_info"`
Securities []ManualSec `json:"securities"`
+1 -3
View File
@@ -329,9 +329,7 @@ func manualFromForm(r *http.Request) *ManualOrder {
RecvINN: f("recv_inn"), RecvName: f("recv_name"), RecvAccount: f("recv_account"), SettlePlace: f("settle_place"),
CostInfo: r.FormValue("cost_info") != "", Comment: f("comment"),
}
if o.SettlePlace != "spb" {
o.SettlePlace = "nrd"
}
o.SettlePlace = "nrd" // СПБ Банк пока не используется
isins, codes, names, qtys := r.Form["sec_isin"], r.Form["sec_code"], r.Form["sec_name"], r.Form["sec_qty"]
at := func(a []string, i int) string {
if i < len(a) {
@@ -51,7 +51,7 @@
<tr><td>Ценная бумага</td><td><b>Одно поручение — одна бумага</b>: если клиент переводит несколько бумаг, на каждую оформляется отдельное поручение. ISIN (только российские выпуски, начинаются с RU) и количество; дробное количество допускается для паёв. Код НРД можно не указывать.</td></tr>
<tr><td>Принимающий депозитарий</td><td>Выбирается из справочника участников M2M, который bj получает от НРД. Если депозитария нет в справочнике, перевод через MOEX МОСТ невозможен.</td></tr>
<tr><td>Счёт у принимающего</td><td>Для сведения: принимающий депозитарий сам находит клиента по документу.</td></tr>
<tr><td>Место расчётов</td><td>НРД или СПБ Банк — как в поручении клиента.</td></tr>
<tr><td>Место расчётов</td><td>НРД.</td></tr>
<tr><td>Сведения о расходах</td><td>Отметьте, если клиент просит передать сведения о расходах на приобретение бумаг.</td></tr>
</tbody></table></div>
<p>Если после сверки или отметок изменить данные поручения, сверка и отметки сбросятся: их нужно пройти заново.</p>
@@ -37,6 +37,12 @@
.mf-hit{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:center;padding:7px 0;border-top:1px solid var(--border)}
.mf-pos{display:block;width:100%;text-align:left;padding:7px 10px;margin:3px 0;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);cursor:pointer;font-size:13px}
.mf-pos:hover:not([disabled]){border-color:var(--accent)}.mf-pos[disabled]{opacity:.5;cursor:not-allowed}
.hb-box{position:relative}
.hb-res{margin-top:4px;max-height:300px;overflow:auto;border-radius:8px}
.hb-res:empty{display:none}
.hb-hit{display:block;width:100%;text-align:left;padding:7px 10px;margin:2px 0;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);cursor:pointer;font-size:13px}
.hb-hit:hover{border-color:var(--accent)}
.hb-m{color:var(--muted);font-size:12px}
.mf-sign{display:grid;grid-template-columns:1fr 1fr;gap:40px;margin-top:40px}
.mf-sign div{border-top:1px solid #000;padding-top:4px;font-size:12px}
@media print{
@@ -114,7 +120,7 @@
<div class="k">Документ</div><div>код {{$o.DocType}} · {{$o.DocSeries}} № {{$o.DocNumber}}{{if $o.DocIssueDate}} · выдан {{$o.DocIssueDate}}{{end}}{{if $o.DocIssuer}} · {{$o.DocIssuer}}{{end}}</div>
<div class="k">Счёт депо / договор</div><div>{{$o.Account}}{{if $o.Contract}} · договор {{$o.Contract}}{{end}}{{if $o.QuikCode}} · код в торговой системе {{$o.QuikCode}}{{end}}</div>
<div class="k">Принимающий депозитарий</div><div>{{$o.RecvName}} · ИНН {{$o.RecvINN}}{{if $o.RecvCode}} · {{$o.RecvCode}}{{end}}{{if $o.RecvAccount}} · счёт клиента {{$o.RecvAccount}}{{end}}</div>
<div class="k">Место расчётов</div><div>{{if eq $o.SettlePlace "spb"}}СПБ Банк{{else}}НРД{{end}}{{if $o.CostInfo}} · со сведениями о расходах{{end}}</div>
<div class="k">Место расчётов</div><div>НРД{{if $o.CostInfo}} · со сведениями о расходах{{end}}</div>
<div class="k">Бумаги</div><div>{{range $o.Securities}}<div>{{.ISIN}}{{if .Code}} ({{.Code}}){{end}}{{if .Name}} — {{.Name}}{{end}} × <b>{{.Qty}}</b></div>{{end}}</div>
{{if $o.Comment}}<div class="k">Комментарий</div><div>{{$o.Comment}}</div>{{end}}
{{if $o.GUID}}<div class="k">Идентификатор перевода</div><div><code>{{$o.GUID}}</code>, отправлено {{msk $o.SentAt}} · {{$o.SentBy}}</div>{{end}}
@@ -332,17 +338,15 @@ function mfRerun(){const f=document.getElementById('mf');if(f&&f.closest('detail
<div class="mf-h">4. Принимающая сторона</div>
<div class="mf-grid">
<div class="wide"><label>Депозитарий из справочника участников M2M</label>
<select id="hb" onchange="mfPickHB(this)">
<option value="">— выбрать —</option>
{{$inn := .O.RecvINN}}{{range .Handbook}}<option value="{{.INN}}" data-name="{{.Name}}" {{if eq $inn .INN}}selected{{end}}>{{.Name}} · ИНН {{.INN}} · {{.Code}}</option>{{end}}
</select>
<div class="wide"><label>Найти в справочнике участников M2M</label>
<input id="hbq" placeholder="ИНН, часть названия или код участника" autocomplete="off">
<div id="hbres" class="hb-res"></div>
{{if not .Handbook}}<span class="mf-note">Справочник участников ещё не получен от НРД — введите ИНН вручную.</span>{{end}}</div>
<div><label class="mf-req">ИНН принимающего депозитария</label><input name="recv_inn" id="recv_inn" value="{{.O.RecvINN}}" maxlength="10" inputmode="numeric"></div>
<div><label>Наименование</label><input name="recv_name" id="recv_name" value="{{.O.RecvName}}"></div>
<div><label>Счёт клиента у принимающего</label><input name="recv_account" value="{{.O.RecvAccount}}" placeholder="для сведения"></div>
<div><label>Место расчётов</label>
<select name="settle_place"><option value="nrd" {{if ne .O.SettlePlace "spb"}}selected{{end}}>НРД</option><option value="spb" {{if eq .O.SettlePlace "spb"}}selected{{end}}>СПБ Банк</option></select></div>
<input value="НРД" disabled></div>
<div class="wide"><label style="display:flex;gap:8px;align-items:center;color:var(--text);font-size:13.5px"><input type="checkbox" name="cost_info" value="1" style="width:auto" {{if .O.CostInfo}}checked{{end}}> Передать сведения о расходах на приобретение (если клиент просил в поручении)</label></div>
</div>
<p class="mf-note">Бумаги списываются с нашего места хранения в НРД: {{if .NSD.DeponentCode}}депонент <code>{{.NSD.DeponentCode}}</code>, счёт <code>{{.NSD.AccountID}}</code>, раздел <code>{{.NSD.SectionID}}</code>{{else}}реквизиты не заданы в настройках НРД{{end}}.</p>
@@ -387,6 +391,24 @@ async function mfPick(i,j){
}
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 mfPickHB(s){const o=s.options[s.selectedIndex];if(!o.value)return;document.getElementById('recv_inn').value=o.value;document.getElementById('recv_name').value=o.dataset.name||''}
// hbFind — подбор участника справочника НРД по ИНН, названию или коду (без учёта регистра и кавычек).
function hbNorm(s){return String(s||'').toLowerCase().replace(/ё/g,'е').replace(/[«»"'“”.,()]/g,' ').replace(/\s+/g,' ').trim()}
function hbFind(list,q){q=hbNorm(q);if(q.length<2)return[];const words=q.split(' ');
return list.map(p=>{const hay=hbNorm([p.inn,p.code,p.name,p.full_name,p.display_name,p.eng_name].join(' '));
if(!words.every(w=>hay.includes(w)))return null;
const score=(String(p.inn).startsWith(q)||String(p.code).toLowerCase().startsWith(q)?0:hbNorm(p.name).startsWith(q)||hbNorm(p.display_name).startsWith(q)?1:2)+(p.kind==='depo'?0:0.5);
return {p,score}}).filter(Boolean).sort((a,b)=>a.score-b.score).slice(0,12).map(x=>x.p)}
function hbBind(inp,box,list,onPick){
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
let hits=[];
const draw=()=>{hits=hbFind(list,inp.value);box.innerHTML=inp.value.trim().length<2?'':(hits.length?hits.map((p,i)=>'<button type="button" class="hb-hit" data-i="'+i+'"><b>'+esc(p.name)+'</b> <span class="hb-m">ИНН '+esc(p.inn)+' · '+esc(p.code)+(p.kind==='broker'?' · брокер':'')+'</span></button>').join(''):'<div class="hb-m" style="padding:6px 2px">В справочнике не найдено — введите ИНН и наименование в поля ниже.</div>')};
inp.addEventListener('input',draw);
inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();if(hits[0]){onPick(hits[0]);box.innerHTML=''}}if(e.key==='Escape')box.innerHTML=''});
box.addEventListener('click',e=>{const b=e.target.closest('.hb-hit');if(!b)return;onPick(hits[+b.dataset.i]);box.innerHTML=''});
}
{
const inp=document.getElementById('hbq');
if(inp)hbBind(inp,document.getElementById('hbres'),{{.Handbook}}||[],p=>{document.getElementById('recv_inn').value=p.inn;document.getElementById('recv_name').value=p.name;inp.value=p.name+' · ИНН '+p.inn})
}
</script>
{{end}}
@@ -479,11 +479,38 @@
{{else}}<p style="margin:8px 0 0;color:var(--err)">Организация не задана — переводы не будут сформированы.</p>{{end}}
<form method="post" action="/admin/setup/org" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<input type="hidden" name="section" value="sec-org">
<label>Из справочника участников M2M</label>
<select name="hb" onchange="var o=this.options[this.selectedIndex];if(o.value){this.form.org_code.value=o.value;this.form.org_inn.value=o.dataset.inn;this.form.org_name.value=o.dataset.name}">
<option value="">— выбрать себя —</option>
{{$code := .OrgCode}}{{range .Participants}}<option value="{{.Code}}" data-inn="{{.INN}}" data-name="{{if .FullName}}{{.FullName}}{{else}}{{.Name}}{{end}}" {{if eq .Code $code}}selected{{end}}>{{.Name}} · ИНН {{.INN}} · {{.Code}}</option>{{end}}
</select>
<label>Найти себя в справочнике участников M2M</label>
<input id="orgq" placeholder="ИНН, часть названия или код участника" autocomplete="off">
<div id="orgres" class="hb-res"></div>
<style>
.hb-box{position:relative}
.hb-res{margin-top:4px;max-height:300px;overflow:auto;border-radius:8px}
.hb-res:empty{display:none}
.hb-hit{display:block;width:100%;text-align:left;padding:7px 10px;margin:2px 0;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);cursor:pointer;font-size:13px}
.hb-hit:hover{border-color:var(--accent)}
.hb-m{color:var(--muted);font-size:12px}
</style>
<script>
// hbFind — подбор участника справочника НРД по ИНН, названию или коду (без учёта регистра и кавычек).
function hbNorm(s){return String(s||'').toLowerCase().replace(/ё/g,'е').replace(/[«»"'“”.,()]/g,' ').replace(/\s+/g,' ').trim()}
function hbFind(list,q){q=hbNorm(q);if(q.length<2)return[];const words=q.split(' ');
return list.map(p=>{const hay=hbNorm([p.inn,p.code,p.name,p.full_name,p.display_name,p.eng_name].join(' '));
if(!words.every(w=>hay.includes(w)))return null;
const score=(String(p.inn).startsWith(q)||String(p.code).toLowerCase().startsWith(q)?0:hbNorm(p.name).startsWith(q)||hbNorm(p.display_name).startsWith(q)?1:2)+(p.kind==='depo'?0:0.5);
return {p,score}}).filter(Boolean).sort((a,b)=>a.score-b.score).slice(0,12).map(x=>x.p)}
function hbBind(inp,box,list,onPick){
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
let hits=[];
const draw=()=>{hits=hbFind(list,inp.value);box.innerHTML=inp.value.trim().length<2?'':(hits.length?hits.map((p,i)=>'<button type="button" class="hb-hit" data-i="'+i+'"><b>'+esc(p.name)+'</b> <span class="hb-m">ИНН '+esc(p.inn)+' · '+esc(p.code)+(p.kind==='broker'?' · брокер':'')+'</span></button>').join(''):'<div class="hb-m" style="padding:6px 2px">В справочнике не найдено — введите ИНН и наименование в поля ниже.</div>')};
inp.addEventListener('input',draw);
inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();if(hits[0]){onPick(hits[0]);box.innerHTML=''}}if(e.key==='Escape')box.innerHTML=''});
box.addEventListener('click',e=>{const b=e.target.closest('.hb-hit');if(!b)return;onPick(hits[+b.dataset.i]);box.innerHTML=''});
}
{
const inp=document.getElementById('orgq'),f=inp.form;
hbBind(inp,document.getElementById('orgres'),{{.Participants}}||[],p=>{f.org_code.value=p.code;f.org_inn.value=p.inn;f.org_name.value=p.full_name||p.name;inp.value=p.name+' · ИНН '+p.inn})
}
</script>
{{if not .Participants}}<span class="muted" style="font-size:12px">Справочник участников ещё не получен — запросите его в «Диагностике» или введите данные вручную.</span>{{end}}
<label>Код участника</label>
<input type="text" name="org_code" value="{{.Settings.NSD.OrgCode}}" placeholder="MC0000000000" maxlength="12">