Мастер, «Организация»: одна кнопка «Сохранить»; в строке справочника — своя запись
- Организация и депозитарные реквизиты в НРД сохраняются одной кнопкой внизу шага (поля реквизитов — в общей форме org-form; saveOrg сохраняет и их, если они пришли; в настройках формы прежние). Повтор «Из DEPO: … подставлено» в мастере убран — то же в зелёной плашке. - «Найти себя в справочнике участников M2M» не пустая: по коду и ИНН из полей показывается своя запись и «✓ Есть в справочнике участников M2M»; нет в справочнике — предупреждение. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -613,3 +613,34 @@ func tsqlOutsideLiterals(s string) string {
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Шаг «Организация» мастера: одна кнопка «Сохранить» — организация и депозитарные реквизиты в НРД одной формой;
|
||||
// без полей реквизитов (настройки) — только организация, реквизиты не трогаются.
|
||||
func TestSaveOrgWithRequisites(t *testing.T) {
|
||||
rc := testRC(t)
|
||||
h := &setupHandlers{rc: rc}
|
||||
post := func(form url.Values) *httptest.ResponseRecorder {
|
||||
r := httptest.NewRequest(http.MethodPost, "/admin/setup/org", strings.NewReader(form.Encode()))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
h.saveOrg(w, r)
|
||||
return w
|
||||
}
|
||||
w := post(url.Values{"org_code": {"MC0413600000"}, "org_inn": {"7703807489"}, "org_name": {"ООО \"ИК \"Фонтвьель\""},
|
||||
"deponent_code": {"MC0413600000"}, "account_id": {"HL171004001C"}, "section_id": {"36MC0413600000F00"}, "return_to": {"/admin/wizard?step=4"}})
|
||||
if loc := w.Header().Get("Location"); !strings.HasPrefix(loc, "/admin/wizard?step=4") {
|
||||
t.Fatalf("после сохранения — на тот же шаг: %q", loc)
|
||||
}
|
||||
s := rc.Snapshot().NSD
|
||||
if s.OrgCode != "MC0413600000" || s.OrgINN != "7703807489" || s.AccountID != "HL171004001C" || s.SectionID != "36MC0413600000F00" || s.DeponentCode != "MC0413600000" {
|
||||
t.Fatalf("организация и реквизиты одной кнопкой: %+v", s)
|
||||
}
|
||||
post(url.Values{"org_code": {"MC0413600000"}, "org_inn": {"7703807489"}, "org_name": {"Фонтвьель"}})
|
||||
if s := rc.Snapshot().NSD; s.AccountID != "HL171004001C" || s.OrgName != "Фонтвьель" {
|
||||
t.Fatalf("форма без реквизитов не должна их стирать: %+v", s)
|
||||
}
|
||||
post(url.Values{"org_code": {"MC0413600000"}, "deponent_code": {"MC0413600000"}, "account_id": {"HL 17"}, "section_id": {""}})
|
||||
if s := rc.Snapshot().NSD; s.AccountID != "HL171004001C" {
|
||||
t.Fatalf("неверный счёт — ничего не сохраняется: %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
+45
-21
@@ -75,13 +75,39 @@ func orgName(s Settings) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// saveOrg — POST /admin/setup/org: организация выбрана из справочника (hb=код) или введена вручную.
|
||||
// saveOrg — POST /admin/setup/org: организация выбрана из справочника (hb=код) или введена вручную. В мастере
|
||||
// та же форма несёт и депозитарные реквизиты в НРД (deponent_code…): на шаге одна кнопка «Сохранить».
|
||||
func (h *setupHandlers) saveOrg(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := h.rc.Snapshot().NSD
|
||||
changedCode, bad := orgFromForm(r, &s)
|
||||
if bad == "" && r.PostForm.Has("deponent_code") {
|
||||
bad = nsdReqFromForm(r, &s)
|
||||
}
|
||||
if bad != "" {
|
||||
setupFlash(w, r, bad)
|
||||
return
|
||||
}
|
||||
if err := h.rc.UpdateNSD(s); err != nil {
|
||||
setupFlash(w, r, "Организация: ошибка сохранения: "+err.Error())
|
||||
return
|
||||
}
|
||||
msg := "Организация сохранена: " + pick(s.OrgName, "—") + ", ИНН " + pick(s.OrgINN, "—") + ", код " + pick(s.OrgCode, "—")
|
||||
if r.PostForm.Has("deponent_code") {
|
||||
msg += ". Реквизиты в НРД: код " + pick(s.DeponentCode, "—") + ", счёт " + pick(s.AccountID, "—") + ", раздел " + pick(s.SectionID, "—")
|
||||
}
|
||||
if changedCode {
|
||||
msg += ". Код отправителя применится после перезапуска bj-server."
|
||||
}
|
||||
setupFlash(w, r, msg)
|
||||
}
|
||||
|
||||
// orgFromForm — код, ИНН и наименование организации из формы (выбор из справочника или вручную).
|
||||
// Возвращает, сменился ли код, и текст ошибки.
|
||||
func orgFromForm(r *http.Request, s *NSDSettings) (bool, string) {
|
||||
code := strings.ToUpper(strings.TrimSpace(r.FormValue("org_code")))
|
||||
inn := strings.TrimSpace(r.FormValue("org_inn"))
|
||||
name := strings.TrimSpace(r.FormValue("org_name"))
|
||||
@@ -91,27 +117,17 @@ func (h *setupHandlers) saveOrg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if inn != "" && !manualINNRe.MatchString(inn) {
|
||||
setupFlash(w, r, "Организация: ИНН юридического лица — 10 цифр")
|
||||
return
|
||||
return false, "Организация: ИНН юридического лица — 10 цифр"
|
||||
}
|
||||
if code != "" && !regexpMCCode.MatchString(code) {
|
||||
setupFlash(w, r, "Организация: код участника — вида MC0000000000")
|
||||
return
|
||||
return false, "Организация: код участника — вида MC0000000000"
|
||||
}
|
||||
changedCode := code != s.OrgCode
|
||||
s.OrgCode, s.OrgINN, s.OrgName = code, inn, name
|
||||
if s.DeponentCode == "" {
|
||||
s.DeponentCode = code
|
||||
}
|
||||
if err := h.rc.UpdateNSD(s); err != nil {
|
||||
setupFlash(w, r, "Организация: ошибка сохранения: "+err.Error())
|
||||
return
|
||||
}
|
||||
msg := "Организация сохранена: " + pick(name, "—") + ", ИНН " + pick(inn, "—") + ", код " + pick(code, "—")
|
||||
if changedCode {
|
||||
msg += ". Код отправителя применится после перезапуска bj-server."
|
||||
}
|
||||
setupFlash(w, r, msg)
|
||||
return changedCode, ""
|
||||
}
|
||||
|
||||
var nsdReqRe = regexp.MustCompile(`^[A-Z0-9]{4,30}$`)
|
||||
@@ -124,19 +140,27 @@ func (h *setupHandlers) saveNSDRequisites(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
s := h.rc.Snapshot().NSD
|
||||
if bad := nsdReqFromForm(r, &s); bad != "" {
|
||||
setupFlash(w, r, bad)
|
||||
return
|
||||
}
|
||||
if err := h.rc.UpdateNSD(s); err != nil {
|
||||
setupFlash(w, r, "Реквизиты НРД: ошибка сохранения: "+err.Error())
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "Реквизиты НРД сохранены: код "+pick(s.DeponentCode, "—")+", счёт "+pick(s.AccountID, "—")+", раздел "+pick(s.SectionID, "—"))
|
||||
}
|
||||
|
||||
// nsdReqFromForm — депозитарный код, счёт и раздел организации в НРД из формы; текст ошибки.
|
||||
func nsdReqFromForm(r *http.Request, s *NSDSettings) string {
|
||||
dep := strings.ToUpper(strings.TrimSpace(r.FormValue("deponent_code")))
|
||||
acc := strings.ToUpper(strings.TrimSpace(r.FormValue("account_id")))
|
||||
sec := strings.ToUpper(strings.TrimSpace(r.FormValue("section_id")))
|
||||
for _, v := range []struct{ name, val string }{{"депозитарный код", dep}, {"депозитарный счёт", acc}, {"раздел счёта", sec}} {
|
||||
if v.val != "" && !nsdReqRe.MatchString(v.val) {
|
||||
setupFlash(w, r, "Реквизиты НРД: "+v.name+" — латинские буквы и цифры, без пробелов")
|
||||
return
|
||||
return "Реквизиты НРД: " + v.name + " — латинские буквы и цифры, без пробелов"
|
||||
}
|
||||
}
|
||||
s.DeponentCode, s.AccountID, s.SectionID = dep, acc, sec
|
||||
if err := h.rc.UpdateNSD(s); err != nil {
|
||||
setupFlash(w, r, "Реквизиты НРД: ошибка сохранения: "+err.Error())
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "Реквизиты НРД сохранены: код "+pick(dep, "—")+", счёт "+pick(acc, "—")+", раздел "+pick(sec, "—"))
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -444,15 +444,14 @@
|
||||
<h2><span class="dot {{if and .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID}}ok{{else}}err{{end}}"></span>Депозитарные реквизиты в НРД</h2>
|
||||
<p class="muted" style="margin:0 0 8px">Где в НРД учитываются бумаги организации — из договора с НРД{{if .Settings.Fancy.Server}} или кнопкой «Определить из DEPO»{{end}}.</p>
|
||||
{{if .Suggest}}{{if .Suggest.AccountID}}<div class="help-block" style="margin:0 0 8px"><strong>Найдено в DEPO:</strong> счёт {{.Suggest.AccountID}}, код {{.Suggest.DeponentCode}}, раздел {{.Suggest.SectionID}}.{{range .Suggest.Notes}}<br><span class="muted">⚠ {{.}}</span>{{end}}</div>{{end}}{{end}}
|
||||
<form method="post" action="/admin/setup/nsd-requisites" style="display:grid;gap:10px;max-width:680px">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=4">
|
||||
<div style="display:grid;gap:10px;max-width:680px">{{/* поля — в общей форме шага (org-form): одна кнопка «Сохранить» */}}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
|
||||
<label style="margin:0">Депозитарный код<input type="text" name="deponent_code" value="{{if .Settings.NSD.DeponentCode}}{{.Settings.NSD.DeponentCode}}{{else if .Suggest}}{{.Suggest.DeponentCode}}{{end}}" placeholder="MC0000000000" style="width:100%;margin-top:4px"></label>
|
||||
<label style="margin:0">Депозитарный счёт<input type="text" name="account_id" value="{{if .Settings.NSD.AccountID}}{{.Settings.NSD.AccountID}}{{else if .Suggest}}{{.Suggest.AccountID}}{{end}}" placeholder="HL000000000C" style="width:100%;margin-top:4px"></label>
|
||||
<label style="margin:0">Раздел счёта<input type="text" name="section_id" value="{{if .Settings.NSD.SectionID}}{{.Settings.NSD.SectionID}}{{else if .Suggest}}{{.Suggest.SectionID}}{{end}}" placeholder="36MC0000000000F00" style="width:100%;margin-top:4px"></label>
|
||||
<label style="margin:0">Депозитарный код<input type="text" name="deponent_code" form="org-form" value="{{if .Settings.NSD.DeponentCode}}{{.Settings.NSD.DeponentCode}}{{else if .Suggest}}{{.Suggest.DeponentCode}}{{end}}" placeholder="MC0000000000" style="width:100%;margin-top:4px"></label>
|
||||
<label style="margin:0">Депозитарный счёт<input type="text" name="account_id" form="org-form" value="{{if .Settings.NSD.AccountID}}{{.Settings.NSD.AccountID}}{{else if .Suggest}}{{.Suggest.AccountID}}{{end}}" placeholder="HL000000000C" style="width:100%;margin-top:4px"></label>
|
||||
<label style="margin:0">Раздел счёта<input type="text" name="section_id" form="org-form" value="{{if .Settings.NSD.SectionID}}{{.Settings.NSD.SectionID}}{{else if .Suggest}}{{.Suggest.SectionID}}{{end}}" placeholder="36MC0000000000F00" style="width:100%;margin-top:4px"></label>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn">Сохранить</button>{{if .Settings.Fancy.Server}}<button type="submit" class="btn" formaction="/admin/setup/fancy/detect" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Определить из DEPO</button>{{end}}</div>
|
||||
</form>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn" form="org-form">Сохранить</button>{{if .Settings.Fancy.Server}}<button type="submit" class="btn" form="org-form" formaction="/admin/setup/fancy/detect" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Определить из DEPO</button>{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "wizNav" .}}
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
<p class="muted">Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах{{if mod "quik-mnp"}}, код отправителя и код фирмы в QUIK{{end}}. Выберите себя из справочника участников НРД или введите данные вручную.</p>{{end}}
|
||||
{{if and .OrgINN .OrgCode}}<p style="margin:8px 0 0"><b>{{if .OrgName}}{{.OrgName}}{{else}}—{{end}}</b> · ИНН <code>{{.OrgINN}}</code> · код <code>{{.OrgCode}}</code>{{if not .Settings.NSD.OrgINN}} <span class="muted">(ИНН взят из справочника по коду)</span>{{end}}</p>
|
||||
{{else if not .InWizard}}<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">
|
||||
<form method="post" action="/admin/setup/org" id="org-form" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
|
||||
<input type="hidden" name="section" value="sec-org">
|
||||
{{if .InWizard}}<input type="hidden" name="return_to" value="/admin/wizard?step=4">{{end}}
|
||||
<label>Найти себя в справочнике участников M2M</label>
|
||||
<div class="hb-wrap"><input id="orgq" placeholder="Выберите из списка или введите ИНН, название, код" autocomplete="off"><button type="button" class="hb-toggle" title="Весь список">▾</button></div>
|
||||
<div id="orgres" class="hb-res"></div>
|
||||
<div id="orgmatch" class="hb-match" hidden></div>
|
||||
<style>
|
||||
.hb-box{position:relative}
|
||||
.hb-res{margin-top:4px;max-height:320px;overflow:auto;border-radius:8px;border:1px solid var(--border);background:var(--card-2);padding:4px}
|
||||
@@ -19,6 +21,7 @@
|
||||
.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}
|
||||
.hb-match{font-size:13px;margin-top:-2px}.hb-match.ok{color:var(--ok)}.hb-match.warn{color:var(--warn)}
|
||||
</style>
|
||||
<script>
|
||||
// hbFind — подбор участника справочника НРД по ИНН, названию или коду (без учёта регистра и кавычек).
|
||||
@@ -49,8 +52,21 @@ function hbBind(inp,box,list,onPick){
|
||||
if(tg)tg.addEventListener('click',()=>{if(box.classList.contains('open'))close();else{inp.focus();draw()}});
|
||||
}
|
||||
{
|
||||
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})
|
||||
const inp=document.getElementById('orgq'),f=inp.form,mt=document.getElementById('orgmatch'),list={{.Participants}}||[];
|
||||
// Организация уже известна (сохранена или найдена в DEPO) — показать её запись из справочника, а не пустую строку.
|
||||
const mark=()=>{
|
||||
const code=f.org_code.value.trim().toUpperCase(),inn=f.org_inn.value.trim();
|
||||
if(!code&&!inn||!list.length){mt.hidden=true;return null}
|
||||
const p=list.find(x=>String(x.code).toUpperCase()===code&&(!inn||String(x.inn)===inn))||(code?list.find(x=>String(x.code).toUpperCase()===code):null);
|
||||
mt.hidden=false;
|
||||
if(p){inp.value=p.name+' · ИНН '+p.inn+' · '+p.code;inp.dataset.picked='1';mt.className='hb-match ok';mt.textContent='✓ Есть в справочнике участников M2M: '+(p.full_name||p.name)+(inn&&String(p.inn)!==inn?' — но ИНН в справочнике '+p.inn+', проверьте':'');}
|
||||
else{mt.className='hb-match warn';mt.textContent='⚠ Участника с кодом '+(code||'—')+' в справочнике M2M нет — проверьте код или выберите себя из списка.';}
|
||||
return p};
|
||||
// поля кода и ИНН — ниже этого скрипта: связать, когда страница загружена
|
||||
const init=()=>{mark();
|
||||
hbBind(inp,document.getElementById('orgres'),list,p=>{f.org_code.value=p.code;f.org_inn.value=p.inn;f.org_name.value=p.full_name||p.name;mark()});
|
||||
f.org_code.addEventListener('change',mark);f.org_inn.addEventListener('change',mark)};
|
||||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
|
||||
}
|
||||
</script>
|
||||
{{if not .Participants}}<span class="muted" style="font-size:12px">Справочник участников ещё не получен — запросите его в «Диагностике» или введите данные вручную.</span>{{end}}
|
||||
@@ -60,8 +76,8 @@ function hbBind(inp,box,list,onPick){
|
||||
<input type="text" name="org_inn" value="{{if .Settings.NSD.OrgINN}}{{.Settings.NSD.OrgINN}}{{else if .Suggest}}{{.Suggest.OrgINN}}{{end}}" maxlength="10" inputmode="numeric">
|
||||
<label>Наименование</label>
|
||||
<input type="text" name="org_name" value="{{if .Settings.NSD.OrgName}}{{.Settings.NSD.OrgName}}{{else if .Suggest}}{{.Suggest.OrgName}}{{end}}">
|
||||
{{if .Suggest}}{{if .Suggest.OrgName}}<span class="muted" style="font-size:12px">Из DEPO: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}} — подставлено в пустые поля.</span>{{end}}{{end}}
|
||||
<div><button type="submit" class="btn">Сохранить</button></div>
|
||||
{{if and .Suggest (not .InWizard)}}{{if .Suggest.OrgName}}<span class="muted" style="font-size:12px">Из DEPO: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}} — подставлено в пустые поля.</span>{{end}}{{end}}
|
||||
{{if not .InWizard}}<div><button type="submit" class="btn">Сохранить</button></div>{{end}}{{/* в мастере — одна кнопка внизу шага: организация и реквизиты НРД вместе */}}
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user