
    Kdj/                     |   S r SSKrSSKrSSKrSSKrSSKrSSKrSSKr\R                  R                  \R                  R                  \5      5      rS(S jrS r\" 5         \" S5      r\" S5      r\" S5      r\" S5      r\" S	5      rS
\ 3rSS/r\" \R0                  R3                  S5      =(       d$    \R4                  R7                  5       R8                  5      rS(S jrS(S jrS)S jr S(S jr!S r"S(S jr#S r$1 Skr%S(S jr&Sr'S r(S(S jr)S r*S r+S r,S r-S r.Sr/S r0\/4S! jr1/ S"Qr2S# r3S$ r4S% r5S& r6S' r7g)*u  Общий слой finsync: .env, клиенты Финтабло и AlfaCRM, пагинация, календарь.

Зачем: раньше клиент Финтабло был скопирован в пяти скриптах, и только в margins были
ретраи и защита «POST не повторяем». Остальные копии при 500-й молча возвращали
{'__error': 500}. Теперь клиент один.

Год НЕ хардкодится: FIN_YEAR = текущий календарный год, переопределяется переменной
окружения FINSYNC_YEAR (полезно, чтобы пересчитать прошлый год).
⚠️ Финмодель в Google Sheets привязана к своему году (колонки B..M = месяцы): при переходе
на новый год нужна новая финмодель, иначе план поедет — сами скрипты при этом работают.
    Nc                    U =(       d$    [         R                  R                  [        S5      n[         R                  R	                  U5      (       d  [        SU S35      e[        U5       nU Hf  nUR                  5       nSU;   d  M  UR                  S5      (       a  M3  UR                  SS5      u  pE[         R                  R                  XE5        Mh     SSS5        g! , (       d  f       g= f)u   Читает .env рядом со скриптом в os.environ (не перетирая уже заданные переменные).z.envu   нет файла u<    — нужны FINTABLO_TOKEN, FINTABLO_BASE_URL, ALFACRM_*=#   N)ospathjoinHEREexistsRuntimeErroropenstrip
startswithsplitenviron
setdefault)r   pfhlinekvs         /home/rasp/finsync/core.pyload_envr      s    *T6*A77>>!/s2nopp	aBD::<Dd{4??3#7#7zz#q)

%%a+	  
s   ,C"
C""7C""
C0c                 n    [         R                  R                  U 5      nU(       d  [        SU  35      eU$ )Nu   в .env нет )r   r   getr   )namer   s     r   envr       s.    


tA_TF344H    FINTABLO_TOKENFINTABLO_BASE_URLALFACRM_HOSTALFACRM_EMAILALFACRM_KEYzhttps://      FINSYNC_YEARc                     U =(       d    [         R                  R                  5       n U R                  S SU R                   3$ )uk   date → 'MM.YYYY' (формат factMonth Финтабло). Без аргумента — сегодня.02d.)datetimedatetodaymonthyeards    r   	month_keyr1   8   s4    	"X]]  "Aggc]!AFF8$$r   c                     U =(       d    [         R                  R                  5       n U R                  SS9[         R                  " SS9-   R                  SS9n[        U5      $ )ut   'MM.YYYY' следующего месяца относительно d (по умолчанию — сегодня).   )day   )daysr   )r*   r+   r,   replace	timedeltar1   )r0   nfs     r   next_month_keyr:   >   sQ    	"X]]  "A
)))
h00a8
8	A	Aa	A	HBR=r   c                 "   U =(       d    [         R                  R                  5       n U=(       d    [        nU R                  U:X  a  U R
                  OU R                  U:  a  SOSn[        SUS-   5       Vs/ s H
  oDS SU 3PM     sn$ s  snf )u  Месяцы года до текущего включительно: ['01.2026', …, '07.2026'].

Считается ОТ КАЛЕНДАРНОЙ ДАТЫ, а не от последней операции в Финтабло: раньше «текущий
месяц» определялся по первой попавшейся операции API, и плановые операции следующего
месяца (isPlan=1, их создаёт plan_sync) уводили весь расчёт на месяц вперёд.
   r   r   r(   r)   )r*   r+   r,   FIN_YEARr.   r-   range)r0   r.   yendms        r   months_to_nowrB   E   su     	
"X]]  "AAVVq[!''QVVaZrQC$)!S1W$56$5qgQqcN$5666s   8Bc                 4    U =(       d    [         nSU 3SU 34$ )ub   ('01.01.YYYY', '31.12.YYYY') — границы года для запросов Финтабло.z01.01.z31.12.r=   r.   r?   s     r   
year_rangerF   R   s#    AA3<6!%%r   c           	          SSK n[        U SS 5      [        U SS 5      p2U SUS S3U SUS SUR                  X25      S   S 34$ )	u&  'MM.YYYY' → ('YYYY-MM-01', 'YYYY-MM-DD') для AlfaCRM.

⚠️ AlfaCRM понимает ТОЛЬКО дефисы: с точками ('2026.07.01') фильтр молча игнорируется
и отдаётся вся история (проверено: total 5917 против 436).
r   Nr$   r%   -r(   z-01r   )calendarint
monthrange)mmrI   mor?   s       r   month_range_alfarN   X   sb     2AKRVS"S!AbXQx/B/B1/I!/LS.QRRRr   c                 4    U =(       d    [         nU S3U S34$ )Nz-01-01z-12-31rD   rE   s     r   year_range_alfarP   c   s#    AS<A3f%%r   c                    [         R                  R                  UR                  5        VVs0 s H  u  p#Uc  M
  X#_M     snn5      n[         R                  R                  [         U  SU 3SS[         30S9nSn[        S5       H6  n [        R                  " [         R                  R                  USS95      s  $    Ues  snnf ! [         R                  R                  [        4 a`  nUn[        U[         R                  R                   5      (       a  UR"                  S	;  a  e [$        R&                  " S
U-   5         SnAM  SnAff = f)u3   GET с ретраями (идемпотентно).N?AuthorizationBearer )headers      timeouti    i  i  i  r   )urllibparse	urlencodeitemsrequestRequestFT_BASEFT_TOKENr>   jsonloadurlopenerrorURLErrorConnectionError
isinstance	HTTPErrorcodetimesleep)	r   ppr   r   qreqlastattemptes	            r   ftgetru   i   s   MqMNA
..
 
 )D61#GH::N(O ! QCD8	$99V^^33C3DEE  J  N %%7 	$D!V\\3344G`9`JJq7{##		$s#   	C
C
1C		$E-AEE>   GETPUTHEADDELETEc                 B   U R                  5       n[        R                  R                  [         U 3U(       a$  [
        R                  " U5      R                  5       OSS[         3SS.U S9nU[        ;   a  SOSnSn[        U5       HL  n [        R                  R                  USS	9nUS
:w  a  [
        R                  " U5      s  $ UR                  s  $    Ue! [        R                  R                  [         4 a  n	U	n[#        U	[        R                  R$                  5      (       a@  US
:X  a)  U	R&                  S:X  a  US:  a  U	R&                  s Sn	A	s  $ U	R&                  S;  a  e XuS-
  :X  a   Sn	A	  Ue[(        R*                  " SU-   5         Sn	A	GM  Sn	A	ff = f)uS  Мутация в Финтабло.

POST НЕ ретраим: если Финтабло записал операцию, а ответ потерялся (таймаут/5xx),
ретрай создал бы ДУБЛЬ плановой операции — это живые деньги. Одиночный POST-сбой
лечится повторным прогоном скрипта: заносы идемпотентны по externalId и пишут журнал
до мутаций, так что потери нет. Идемпотентные методы (DELETE/PUT) ретраим.
NrT   application/json)rS   Content-Type)datarU   methodrV   r   rW   rX   ry   i  r   rZ   )upperr\   r`   ra   rb   rd   dumpsencoderc   _IDEMPOTENTr>   rf   re   statusrg   rh   ri   rj   rk   rl   rm   rn   )
r~   r   bodyuprq   attemptsrr   rs   rrt   s
             r   ftmutr      sY    
B
..
 
 )D6dD!1!8!8!:$+H:"6HZ[dj ! lC +%q1HD?	$&&sB&7A#%>499Q<?qxx? # J %%7 
	$D!V\\3344>affm!66M66!::Q,&
J JJq7{##
	$s1   9C	C$F?AFFF9FFi  c                     / Sp2 [        U 4[        US.UD6nUR                  S/ 5      nX%-  n[        U5      [        :  a  U$ US-  nME  )uS   Все страницы Финтабло одним списком (pageSize=1000).r   pageSizepager_   )ru   FT_PAGEr   len)r   ro   outpgr0   its         r   ft_allr      sX    !
$8r8R8UU7B	r7WJ
a r   c                     UR                  S[        U 5      S   5        UR                  S[        U 5      S   5        [        S0 UD6$ )u   Операции Финтабло за год (по умолчанию FIN_YEAR). Фильтры — как у /v1/transaction.dateFromr   dateTor   )z/v1/transaction)r   rF   r   )r.   ro   s     r   ft_transactionsr      s@    MM*j.q12MM(Jt,Q/0*r**r   c                  p    [        S5      R                  S/ 5       V s0 s H  o S   U S   _M     sn $ s  sn f )Nz/v1/categoryr_   idr   ru   r   )cs    r   ft_categoriesr      s9    (-n(=(A(A'2(NO(N1dGQvY(NOOOs   3c                      [        S5      R                  S/ 5       V s0 s H,  o R                  S5      U R                  S5      =(       d    S_M.     sn $ s  sn f )u+   {partnerId: имя контрагента}.z/v1/partnerr_   r   r    r   )r   s    r   ft_partnersr      sH    8=m8L8P8PQXZ\8]^8]1EE$K!%%--2-8]^^^s   3Ac                 F   ^  [        U 4S j[        S5      S    5       5      $ )Nc              3   B   >#    U  H  oS    T:X  d  M  US   v   M     g7f)r   balanceN ).0xmb_ids     r   	<genexpr>&ft_moneybag_balance.<locals>.<genexpr>   s#     Y&D$SXHX)&Ds   z/v1/moneybagr_   )nextru   )r   s   `r   ft_moneybag_balancer      s    YeN&;G&DYYYr   c                    SS0nU(       a  X#S'   [         R                  R                  [        U -   [        R
                  " U5      R                  5       SUS9nSn[        S5       H6  n [        R                  " [         R                  R                  USS	95      s  $    [!        SU  SU S35      e! [         R                  R                  [        4 az  nUn[        U[         R                  R                  5      (       a+  UR                  S
;  a  [!        SU  SUR                   35      e["        R$                  " SU-   5         SnAM  SnAff = f)uk  POST в AlfaCRM. Ошибку НЕ проглатываем.

Раньше после исчерпания ретраев возвращалось пустое {} — вызывающий видел items=[]
и считал, что страницы кончились: часть выручки молча терялась. Теперь — исключение.
r|   r{   zX-ALFACRM-TOKENPOST)r}   r~   rU   N   (   rX   rZ   AlfaCRM z: HTTP r   u3   : не ответил после 4 попыток ())r\   r`   ra   A_BASErd   r   r   r>   re   rf   rg   rh   ri   rj   rk   rl   r   rm   rn   )r   r   tokenhdrrq   rr   rs   rt   s           r   alfar      s&    -
.C!&
..
 
 $TZZ5E5L5L5N(. ! =CD8	$99V^^33C3DEE  $'Z[_Z``ab
cc %%7 	$D!V\\3344G`9`"XdV7166(#CDDJJq7{##		$s   *1B22$EA/EEc                  x    [        S[        [        S.S 5      R                  S5      n U (       d  [	        S5      eU $ )Nz/v2api/auth/login)emailapi_keyr   u^   AlfaCRM: авторизация не удалась (проверь ALFACRM_EMAIL/ALFACRM_KEY))r   A_EMAILA_KEYr   r   )toks    r   
alfa_loginr      s6    
"g%$H$
O
S
ST[
\C{||Jr   r[      c           	         / SpTU[         :  ah  [        U [        XUS9U5      nUR                  S/ 5      nXG-  nUR                  S5      nU(       a  Ub  [	        U5      U:  a  U$ US-  nU[         :  a  Mh  [        SU  S[          S35      e)	u\  Все страницы AlfaCRM одним списком.

Признак конца — `total` из ответа (в AlfaCRM `count` = размер СТРАНИЦЫ, не всего).
Упереться в потолок страниц молча нельзя: это была бы тихая потеря данных → исключение.
r   r   r_   totalr   r   u   : больше u@    страниц — данные могли обрезаться)ALFA_MAX_PAGESr   dictr   r   r   )	r   r   r   	page_sizer   r   r   r_   r   s	            r   alfa_allr      s     A

tD4@%Hgr"g*s3x5/@J	 
 $~6F  GG  H  I  Ir   )r   u   январяu   февраляu
   мартаu   апреляu   маяu   июняu   июляu   августаu   сентябряu   октябряu   ноябряu   декабряc                 F    U R                    S[        U R                      3$ )u   date → «25 июля» (раньше месяц переводился через .replace('July','июля') — работало
ровно два месяца в году). )r4   RU_MONTH_GENr-   r/   s    r   ru_dater      s#     eeWAl177+,--r   c                 ,    [         [        U SS 5         $ )u   '07.2026' → 'июля'.Nr$   )r   rJ   )rL   s    r   ru_monthr     s    BrF$$r   c                 <    [        U 5      S R                  SS5      $ )u   1234567 → '1 234 567'.,r   )roundr7   )r   s    r   moneyr     s    Ahq\""3,,r   c                     SSK nU =(       d    SR                  SS5      R                  SS5      R                  SS5      n UR                  SU 5      nU(       a  [        US   5      $ S	$ )
ui   Первое число из строки таблицы ('1 234,50 ₽' → 1234.5). Мусор → 0.0.r   Nr       r   r   r)   z-?\d+\.?\d*g        )rer7   findallfloat)sr   rL   s      r   numr     sZ    	
b&"%--c26>>sCHA	NA	&B5A<&3&r   c                     SSK nUR                  SS[        U =(       d    S5      R                  5       5      R	                  5       R                  SS5      $ )uq   Нормализация имени статьи/строки: схлопнуть пробелы, lower, ё→е.r   Nz\s+r   r   u   ёu   е)r   substrr   lowerr7   )r   r   s     r   normr     s?    66&#s17|1134::<DDT4PPr   )N)NN)8__doc__r   rd   rm   r*   urllib.requestr\   urllib.parseurllib.errorr   dirnameabspath__file__r
   r   r   rc   rb   A_HOSTr   r   r   BRANCHESrJ   r   r   r+   r,   r.   r=   r1   r:   rB   rF   rN   rP   ru   r   r   r   r   r   r   r   r   r   r   	ALFA_PAGEr   r   r   r   r   r   r   r   r   r   r   <module>r      s]  
 L K K K	wwrwwx01
, 	
 
!
"	^	
o
MF8	 q6 rzz~~n-K1D1D1F1K1KL%
7&S&& /@ 	+P_
Z
d. 	 +4 I&v.%-
'Qr   