stage-1: project bootstrap
Structure, config loader, personality/tools/memory from clawd, venv, 22 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
59
tools/anaf-monitor/config.json
Normal file
59
tools/anaf-monitor/config.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"id": "D100",
|
||||
"name": "Declarația 100 - Obligații de plată la bugetul de stat",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/100.html"
|
||||
},
|
||||
{
|
||||
"id": "D101",
|
||||
"name": "Declarația 101 - Impozit pe profit",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/101.html"
|
||||
},
|
||||
{
|
||||
"id": "D300",
|
||||
"name": "Declarația 300 - Decont TVA",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/300.html"
|
||||
},
|
||||
{
|
||||
"id": "D390",
|
||||
"name": "Declarația 390 - Recapitulativă livrări/achiziții intracomunitare",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/390.html"
|
||||
},
|
||||
{
|
||||
"id": "D394",
|
||||
"name": "Declarația 394 - Informativă livrări/achiziții",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/394.html"
|
||||
},
|
||||
{
|
||||
"id": "D205",
|
||||
"name": "Declarația 205 - Informativă impozit la sursă",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/205.html"
|
||||
},
|
||||
{
|
||||
"id": "D406",
|
||||
"name": "Declarația 406 - SAF-T",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/406.html"
|
||||
},
|
||||
{
|
||||
"id": "BILANT_2025",
|
||||
"name": "Bilanț 31.12.2025 (S1002-S1005)",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/situatiifinanciare/2025/1002_5_2025.html"
|
||||
},
|
||||
{
|
||||
"id": "SIT_FIN_SEM_2025",
|
||||
"name": "Raportări contabile semestriale 2025",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/situatiifinanciare/2025/semestriale/1012_2025.html"
|
||||
},
|
||||
{
|
||||
"id": "SIT_FIN_AN_2025",
|
||||
"name": "Situații financiare anuale 2025",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/situatiifinanciare/2025/1030_2025.html"
|
||||
},
|
||||
{
|
||||
"id": "DESCARCARE_DECLARATII",
|
||||
"name": "Pagina principală descărcare declarații",
|
||||
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/descarcare_declaratii.htm"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tools/anaf-monitor/hashes.json
Normal file
14
tools/anaf-monitor/hashes.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"D100": "44c03d855b36c32578b58bef6116e861c1d26ed6b038d732c23334b5d42f20de",
|
||||
"D101": "937209d4785ca013cbcbe5a0d0aa8ba0e7033d3d8e6c121dadd8e38b20db8026",
|
||||
"D300": "1349f3b1b4db7fe51ff82b0a91db44b16db83e843c56b0568e42ff3090a94f59",
|
||||
"D394": "c4c4e62bda30032f12c17edf9a5087b6173a350ccb1fd750158978b3bd0acb7d",
|
||||
"D406": "5a6712fab7b904ee659282af1b62f8b789aada5e3e4beb9fcce4ea3e0cab6ece",
|
||||
"SIT_FIN_SEM_2025": "8164843431e6b703a38fbdedc7898ec6ae83559fe10f88663ba0b55f3091d5fe",
|
||||
"SIT_FIN_AN_2025": "c00c39079482af8b7af6d32ba7b85c7d9e8cb25ebcbd6704adabd0192e1adca8",
|
||||
"DESCARCARE_DECLARATII": "d66297abcfc2b3ad87f65e4a60c97ddd0a889f493bb7e7c8e6035ef39d55ec3f",
|
||||
"D205": "f707104acc691cf79fbaa9a80c68bff4a285297f7dd3ab7b7a680715b54fd502",
|
||||
"D390": "4726938ed5858ec735caefd947a7d182b6dc64009478332c4feabdb36412a84e",
|
||||
"BILANT_2024": "fbb8d66c2e530d8798362992c6983e07e1250188228c758cb6da4cde4f955950",
|
||||
"BILANT_2025": "9d66ffa59b8be06a5632b0f23a0354629f175ae5204398d7bb7a4c4734d5275a"
|
||||
}
|
||||
450
tools/anaf-monitor/monitor.log
Normal file
450
tools/anaf-monitor/monitor.log
Normal file
@@ -0,0 +1,450 @@
|
||||
[2026-01-29 00:38:12] === Starting ANAF monitor check ===
|
||||
[2026-01-29 00:38:12] === Monitor check complete ===
|
||||
[2026-01-29 00:38:32] === Starting ANAF monitor check ===
|
||||
[2026-01-29 00:38:32] INIT: D100 - storing initial hash
|
||||
[2026-01-29 00:38:32] INIT: D101 - storing initial hash
|
||||
[2026-01-29 00:38:32] INIT: D200 - storing initial hash
|
||||
[2026-01-29 00:38:32] INIT: D390 - storing initial hash
|
||||
[2026-01-29 00:38:32] INIT: D406 - storing initial hash
|
||||
[2026-01-29 00:38:32] INIT: EFACTURA - storing initial hash
|
||||
[2026-01-29 00:38:32] INIT: SIT_FIN_SEM_2025 - storing initial hash
|
||||
[2026-01-29 00:38:32] INIT: SIT_FIN_AN_2025 - storing initial hash
|
||||
[2026-01-29 00:38:33] INIT: SIT_FIN_AN_2024 - storing initial hash
|
||||
[2026-01-29 00:38:33] INIT: DESCARCARE_DECLARATII - storing initial hash
|
||||
[2026-01-29 00:38:33] === Monitor check complete ===
|
||||
[2026-01-29 00:46:30] === Starting ANAF monitor check ===
|
||||
[2026-01-29 00:46:30] INIT: D100 - storing initial hash
|
||||
[2026-01-29 00:46:30] INIT: D101 - storing initial hash
|
||||
[2026-01-29 00:46:30] INIT: D300 - storing initial hash
|
||||
[2026-01-29 00:46:30] INIT: D394 - storing initial hash
|
||||
[2026-01-29 00:46:30] INIT: D406 - storing initial hash
|
||||
[2026-01-29 00:46:30] INIT: SIT_FIN_SEM_2025 - storing initial hash
|
||||
[2026-01-29 00:46:30] INIT: SIT_FIN_AN_2025 - storing initial hash
|
||||
[2026-01-29 00:46:31] INIT: DESCARCARE_DECLARATII - storing initial hash
|
||||
[2026-01-29 00:46:31] === Monitor check complete ===
|
||||
[2026-01-29 12:31:52] === Starting ANAF monitor check ===
|
||||
[2026-01-29 12:31:52] OK: D100 - no changes
|
||||
[2026-01-29 12:31:52] OK: D101 - no changes
|
||||
[2026-01-29 12:31:53] OK: D300 - no changes
|
||||
[2026-01-29 12:31:53] OK: D394 - no changes
|
||||
[2026-01-29 12:31:53] INIT: D205 - storing initial hash
|
||||
[2026-01-29 12:31:53] OK: D406 - no changes
|
||||
[2026-01-29 12:31:53] OK: SIT_FIN_SEM_2025 - no changes
|
||||
[2026-01-29 12:31:53] OK: SIT_FIN_AN_2025 - no changes
|
||||
[2026-01-29 12:31:53] OK: DESCARCARE_DECLARATII - no changes
|
||||
[2026-01-29 12:31:53] === Monitor check complete ===
|
||||
[2026-01-29 12:32:10] === Starting ANAF monitor check ===
|
||||
[2026-01-29 12:32:10] OK: D100 - no changes
|
||||
[2026-01-29 12:32:10] OK: D101 - no changes
|
||||
[2026-01-29 12:32:10] OK: D300 - no changes
|
||||
[2026-01-29 12:32:11] INIT: D390 - storing initial hash
|
||||
[2026-01-29 12:32:11] OK: D394 - no changes
|
||||
[2026-01-29 12:32:11] OK: D205 - no changes
|
||||
[2026-01-29 12:32:11] OK: D406 - no changes
|
||||
[2026-01-29 12:32:13] OK: SIT_FIN_SEM_2025 - no changes
|
||||
[2026-01-29 12:32:13] OK: SIT_FIN_AN_2025 - no changes
|
||||
[2026-01-29 12:32:14] OK: DESCARCARE_DECLARATII - no changes
|
||||
[2026-01-29 12:32:14] === Monitor check complete ===
|
||||
[2026-01-29 12:51:27] === Starting ANAF monitor check ===
|
||||
[2026-01-29 12:51:27] OK: D100 - no changes
|
||||
[2026-01-29 12:51:27] OK: D101 - no changes
|
||||
[2026-01-29 12:51:27] OK: D300 - no changes
|
||||
[2026-01-29 12:51:27] OK: D390 - no changes
|
||||
[2026-01-29 12:51:28] OK: D394 - no changes
|
||||
[2026-01-29 12:51:28] OK: D205 - no changes
|
||||
[2026-01-29 12:51:28] OK: D406 - no changes
|
||||
[2026-01-29 12:51:28] INIT: BILANT_2024 - storing initial hash
|
||||
[2026-01-29 12:51:28] OK: SIT_FIN_SEM_2025 - no changes
|
||||
[2026-01-29 12:51:28] OK: SIT_FIN_AN_2025 - no changes
|
||||
[2026-01-29 12:51:28] OK: DESCARCARE_DECLARATII - no changes
|
||||
[2026-01-29 12:51:28] === Monitor check complete ===
|
||||
[2026-01-29 12:51:53] === Starting ANAF monitor check ===
|
||||
[2026-01-29 12:51:53] OK: D100 - no changes
|
||||
[2026-01-29 12:51:53] OK: D101 - no changes
|
||||
[2026-01-29 12:51:53] OK: D300 - no changes
|
||||
[2026-01-29 12:51:53] OK: D390 - no changes
|
||||
[2026-01-29 12:51:53] OK: D394 - no changes
|
||||
[2026-01-29 12:51:53] OK: D205 - no changes
|
||||
[2026-01-29 12:51:53] OK: D406 - no changes
|
||||
[2026-01-29 12:51:53] INIT: BILANT_2025 - storing initial hash
|
||||
[2026-01-29 12:51:53] OK: SIT_FIN_SEM_2025 - no changes
|
||||
[2026-01-29 12:51:53] OK: SIT_FIN_AN_2025 - no changes
|
||||
[2026-01-29 12:51:54] OK: DESCARCARE_DECLARATII - no changes
|
||||
[2026-01-29 12:51:54] === Monitor check complete ===
|
||||
[2026-01-29 12:55:43] === Starting ANAF monitor v2 check ===
|
||||
[2026-01-29 12:55:43] OK: D100 - no changes
|
||||
[2026-01-29 12:55:43] OK: D101 - no changes
|
||||
[2026-01-29 12:55:43] OK: D300 - no changes
|
||||
[2026-01-29 12:55:43] OK: D390 - no changes
|
||||
[2026-01-29 12:55:44] OK: D394 - no changes
|
||||
[2026-01-29 12:55:44] OK: D205 - no changes
|
||||
[2026-01-29 12:55:44] OK: D406 - no changes
|
||||
[2026-01-29 12:55:44] OK: BILANT_2025 - no changes
|
||||
[2026-01-29 12:55:44] OK: SIT_FIN_SEM_2025 - no changes
|
||||
[2026-01-29 12:55:44] OK: SIT_FIN_AN_2025 - no changes
|
||||
[2026-01-29 12:55:44] OK: DESCARCARE_DECLARATII - no changes
|
||||
[2026-01-29 12:55:44] === Monitor check complete ===
|
||||
[2026-01-29 12:56:25] === Starting ANAF monitor v2 ===
|
||||
[2026-01-29 12:56:27] INIT: D100 - {'soft_a_url': 'http://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D100_710_XML_0126_260126.pdf', 'soft_a_date': '26.01.2026', 'soft_j_url': 'http://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D100_22012026.zip', 'soft_j_date': '22.01.2026'}
|
||||
[2026-01-29 12:56:27] INIT: D101 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D101_XML_2025_260126.pdf', 'soft_a_date': '26.01.2026', 'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D101_J1102.zip'}
|
||||
[2026-01-29 12:56:27] INIT: D300 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D300_v11.0.7_16122025.pdf', 'soft_a_date': '16.12.2025', 'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D300_20250910.zip', 'soft_j_date': '10.09.2025'}
|
||||
[2026-01-29 12:56:27] INIT: D390 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D390_XML_2020_300424.pdf', 'soft_a_date': '30.04.2024', 'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D390_20250625.zip', 'soft_j_date': '25.06.2025'}
|
||||
[2026-01-29 12:56:27] INIT: D394 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D394_26092025.pdf', 'soft_a_date': '26.09.2025', 'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D394_17092025.zip', 'soft_j_date': '17.09.2025'}
|
||||
[2026-01-29 12:56:27] INIT: D205 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D205_XML_2025_150126.pdf', 'soft_a_date': '15.01.2026', 'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D205_J901_P400.zip'}
|
||||
[2026-01-29 12:56:27] INIT: D406 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/R405_XML_2017_080321.pdf', 'soft_a_date': '08.03.2021', 'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D406_20251030.zip', 'soft_j_date': '30.10.2025'}
|
||||
[2026-01-29 12:56:27] INIT: BILANT_2025 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/bilant_SC_1225_XML_270126.pdf', 'soft_a_date': '27.01.2026', 'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/S1002_20260128.zip', 'soft_j_date': '28.01.2026'}
|
||||
[2026-01-29 12:56:28] INIT: SIT_FIN_SEM_2025 - {'soft_j_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/S1012_20250723.zip', 'soft_j_date': '23.07.2025'}
|
||||
[2026-01-29 12:56:28] INIT: SIT_FIN_AN_2025 - {'soft_a_url': 'https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/bilant_S1030_XML_consolidare_270126_bis.pdf', 'soft_a_date': '27.01.2026'}
|
||||
[2026-01-29 12:56:28] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-01-29 12:56:28] === Monitor complete ===
|
||||
[2026-01-29 13:28:32] === Starting ANAF monitor v2 ===
|
||||
[2026-01-29 13:28:32] OK: D100
|
||||
[2026-01-29 13:28:32] OK: D101
|
||||
[2026-01-29 13:28:32] OK: D300
|
||||
[2026-01-29 13:28:32] OK: D390
|
||||
[2026-01-29 13:28:32] OK: D394
|
||||
[2026-01-29 13:28:33] OK: D205
|
||||
[2026-01-29 13:28:33] OK: D406
|
||||
[2026-01-29 13:28:33] OK: BILANT_2025
|
||||
[2026-01-29 13:28:33] OK: SIT_FIN_SEM_2025
|
||||
[2026-01-29 13:28:33] OK: SIT_FIN_AN_2025
|
||||
[2026-01-29 13:28:33] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-01-29 13:28:33] === Monitor complete ===
|
||||
[2026-01-31 06:30:14] === Starting ANAF monitor v2 ===
|
||||
[2026-01-31 06:30:15] OK: D100
|
||||
[2026-01-31 06:30:15] OK: D101
|
||||
[2026-01-31 06:30:15] OK: D300
|
||||
[2026-01-31 06:30:15] OK: D390
|
||||
[2026-01-31 06:30:16] OK: D394
|
||||
[2026-01-31 06:30:16] OK: D205
|
||||
[2026-01-31 06:30:17] OK: D406
|
||||
[2026-01-31 06:30:17] OK: BILANT_2025
|
||||
[2026-01-31 06:30:17] OK: SIT_FIN_SEM_2025
|
||||
[2026-01-31 06:30:17] OK: SIT_FIN_AN_2025
|
||||
[2026-01-31 06:30:17] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-01-31 06:30:17] === Monitor complete ===
|
||||
[2026-01-31 13:43:41] === Starting ANAF monitor v2 ===
|
||||
[2026-01-31 13:43:41] OK: D100
|
||||
[2026-01-31 13:43:41] OK: D101
|
||||
[2026-01-31 13:43:41] OK: D300
|
||||
[2026-01-31 13:43:42] OK: D390
|
||||
[2026-01-31 13:43:42] OK: D394
|
||||
[2026-01-31 13:43:42] OK: D205
|
||||
[2026-01-31 13:43:42] OK: D406
|
||||
[2026-01-31 13:43:42] OK: BILANT_2025
|
||||
[2026-01-31 13:43:42] OK: SIT_FIN_SEM_2025
|
||||
[2026-01-31 13:43:42] OK: SIT_FIN_AN_2025
|
||||
[2026-01-31 13:43:42] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-01-31 13:43:42] === Monitor complete ===
|
||||
[2026-02-01 06:30:07] === Starting ANAF monitor v2 ===
|
||||
[2026-02-01 06:30:07] OK: D100
|
||||
[2026-02-01 06:30:07] OK: D101
|
||||
[2026-02-01 06:30:07] OK: D300
|
||||
[2026-02-01 06:30:07] OK: D390
|
||||
[2026-02-01 06:30:08] OK: D394
|
||||
[2026-02-01 06:30:08] OK: D205
|
||||
[2026-02-01 06:30:08] OK: D406
|
||||
[2026-02-01 06:30:08] OK: BILANT_2025
|
||||
[2026-02-01 06:30:08] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-01 06:30:08] OK: SIT_FIN_AN_2025
|
||||
[2026-02-01 06:30:08] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-02-01 06:30:08] === Monitor complete ===
|
||||
[2026-02-02 06:30:06] === Starting ANAF monitor v2 ===
|
||||
[2026-02-02 06:30:06] OK: D100
|
||||
[2026-02-02 06:30:06] OK: D101
|
||||
[2026-02-02 06:30:07] OK: D300
|
||||
[2026-02-02 06:30:07] OK: D390
|
||||
[2026-02-02 06:30:07] OK: D394
|
||||
[2026-02-02 06:30:07] OK: D205
|
||||
[2026-02-02 06:30:07] OK: D406
|
||||
[2026-02-02 06:30:07] OK: BILANT_2025
|
||||
[2026-02-02 06:30:08] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-02 06:30:08] OK: SIT_FIN_AN_2025
|
||||
[2026-02-02 06:30:08] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-02-02 06:30:08] === Monitor complete ===
|
||||
[2026-02-02 07:11:47] === Starting ANAF monitor v2 ===
|
||||
[2026-02-02 07:11:47] OK: D100
|
||||
[2026-02-02 07:11:47] OK: D101
|
||||
[2026-02-02 07:11:47] OK: D300
|
||||
[2026-02-02 07:11:47] OK: D390
|
||||
[2026-02-02 07:11:48] OK: D394
|
||||
[2026-02-02 07:11:48] OK: D205
|
||||
[2026-02-02 07:11:48] OK: D406
|
||||
[2026-02-02 07:11:48] OK: BILANT_2025
|
||||
[2026-02-02 07:11:48] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-02 07:11:48] OK: SIT_FIN_AN_2025
|
||||
[2026-02-02 07:11:48] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-02-02 07:11:48] === Monitor complete ===
|
||||
[2026-02-02 07:52:10] === Starting ANAF monitor v2 ===
|
||||
[2026-02-02 07:52:10] OK: D100
|
||||
[2026-02-02 07:52:10] OK: D101
|
||||
[2026-02-02 07:52:10] OK: D300
|
||||
[2026-02-02 07:52:10] OK: D390
|
||||
[2026-02-02 07:52:10] OK: D394
|
||||
[2026-02-02 07:52:10] OK: D205
|
||||
[2026-02-02 07:52:10] OK: D406
|
||||
[2026-02-02 07:52:10] OK: BILANT_2025
|
||||
[2026-02-02 07:52:10] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-02 07:52:11] OK: SIT_FIN_AN_2025
|
||||
[2026-02-02 07:52:11] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-02-02 07:52:11] === Monitor complete ===
|
||||
[2026-02-03 21:34:59] === Starting ANAF monitor v2 ===
|
||||
[2026-02-03 21:35:00] CHANGES in D100: ['Soft A: 26.01.2026 → 02.02.2026']
|
||||
[2026-02-03 21:35:00] OK: D101
|
||||
[2026-02-03 21:35:00] OK: D300
|
||||
[2026-02-03 21:35:00] OK: D390
|
||||
[2026-02-03 21:35:00] OK: D394
|
||||
[2026-02-03 21:35:00] OK: D205
|
||||
[2026-02-03 21:35:00] OK: D406
|
||||
[2026-02-03 21:35:00] OK: BILANT_2025
|
||||
[2026-02-03 21:35:00] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-03 21:35:01] OK: SIT_FIN_AN_2025
|
||||
[2026-02-03 21:35:01] INIT: DESCARCARE_DECLARATII - {}
|
||||
[2026-02-03 21:35:01] === Monitor complete ===
|
||||
[2026-02-03 21:37:42] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-03 21:37:43] CHANGES in D100: ['Soft A 10: 17.11.2016 (NOU)', 'Soft A 11: 15.04.2016 (NOU)', 'Soft A 12: 19.01.2015 (NOU)', 'Soft A 14: 28.12.2012 (NOU)', 'Soft A 2: 25.01.2024 (NOU)', 'Soft A 3: 20.01.2022 (NOU)', 'Soft A 4: 23.10.2020 (NOU)', 'Soft A 5: 30.09.2020 (NOU)', 'Soft A 6: 03.12.2019 (NOU)', 'Soft A 7: 01.02.2018 (NOU)', 'Soft A 8: 17.10.2017 (NOU)', 'Soft A 9: 01.08.2017 (NOU)', 'Soft J 10: 19.01.2015 (NOU)', 'Soft J 2: 17.01.2024 (NOU)', 'Soft J 3: 20.01.2022 (NOU)', 'Soft J 4: 30.09.2020 (NOU)', 'Soft J 5: 28.01.2020 (NOU)', 'Soft J 6: 01.02.2018 (NOU)', 'Soft J 7: 17.10.2017 (NOU)', 'Soft J 8: 01.08.2017 (NOU)', 'Soft J 9: 21.03.2016 (NOU)']
|
||||
[2026-02-03 21:37:43] OK: D101
|
||||
[2026-02-03 21:37:43] OK: D300
|
||||
[2026-02-03 21:37:43] OK: D390
|
||||
[2026-02-03 21:37:43] OK: D394
|
||||
[2026-02-03 21:37:43] OK: D205
|
||||
[2026-02-03 21:37:43] OK: D406
|
||||
[2026-02-03 21:37:43] CHANGES in BILANT_2025: ['Soft J S1002: 28.01.2026 (NOU)', 'Soft J S1003: 04.02.2025 (NOU)', 'Soft J S1004: 04.02.2025 (NOU)']
|
||||
[2026-02-03 21:37:43] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-03 21:37:44] OK: SIT_FIN_AN_2025
|
||||
[2026-02-03 21:37:44] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-03 21:37:44] === Monitor complete ===
|
||||
[2026-02-03 21:38:07] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-03 21:38:07] OK: D100
|
||||
[2026-02-03 21:38:08] OK: D101
|
||||
[2026-02-03 21:38:08] OK: D300
|
||||
[2026-02-03 21:38:08] OK: D390
|
||||
[2026-02-03 21:38:08] OK: D394
|
||||
[2026-02-03 21:38:08] OK: D205
|
||||
[2026-02-03 21:38:08] OK: D406
|
||||
[2026-02-03 21:38:08] OK: BILANT_2025
|
||||
[2026-02-03 21:38:08] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-03 21:38:08] OK: SIT_FIN_AN_2025
|
||||
[2026-02-03 21:38:08] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-03 21:38:08] === Monitor complete ===
|
||||
[2026-02-03 21:39:16] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-03 21:39:16] OK: D100
|
||||
[2026-02-03 21:39:16] OK: D101
|
||||
[2026-02-03 21:39:17] OK: D300
|
||||
[2026-02-03 21:39:17] OK: D390
|
||||
[2026-02-03 21:39:17] OK: D394
|
||||
[2026-02-03 21:39:17] OK: D205
|
||||
[2026-02-03 21:39:17] OK: D406
|
||||
[2026-02-03 21:39:17] OK: BILANT_2025
|
||||
[2026-02-03 21:39:17] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-03 21:39:17] OK: SIT_FIN_AN_2025
|
||||
[2026-02-03 21:39:17] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-03 21:39:17] === Monitor complete ===
|
||||
[2026-02-04 08:00:20] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-04 08:00:20] OK: D100
|
||||
[2026-02-04 08:00:20] OK: D101
|
||||
[2026-02-04 08:00:20] OK: D300
|
||||
[2026-02-04 08:00:20] OK: D390
|
||||
[2026-02-04 08:00:20] OK: D394
|
||||
[2026-02-04 08:00:20] OK: D205
|
||||
[2026-02-04 08:00:20] OK: D406
|
||||
[2026-02-04 08:00:21] OK: BILANT_2025
|
||||
[2026-02-04 08:00:21] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-04 08:00:21] OK: SIT_FIN_AN_2025
|
||||
[2026-02-04 08:00:21] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-04 08:00:21] === Monitor complete ===
|
||||
[2026-02-05 14:00:09] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-05 14:00:09] OK: D100
|
||||
[2026-02-05 14:00:09] OK: D101
|
||||
[2026-02-05 14:00:09] OK: D300
|
||||
[2026-02-05 14:00:09] OK: D390
|
||||
[2026-02-05 14:00:09] OK: D394
|
||||
[2026-02-05 14:00:09] OK: D205
|
||||
[2026-02-05 14:00:10] OK: D406
|
||||
[2026-02-05 14:00:10] HASH CHANGED in BILANT_2025 (no version changes detected)
|
||||
[2026-02-05 14:00:10] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-05 14:00:10] HASH CHANGED in SIT_FIN_AN_2025 (no version changes detected)
|
||||
[2026-02-05 14:00:10] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-05 14:00:10] === Monitor complete ===
|
||||
[2026-02-06 08:00:10] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-06 08:00:10] OK: D100
|
||||
[2026-02-06 08:00:10] OK: D101
|
||||
[2026-02-06 08:00:10] OK: D300
|
||||
[2026-02-06 08:00:10] OK: D390
|
||||
[2026-02-06 08:00:10] OK: D394
|
||||
[2026-02-06 08:00:10] OK: D205
|
||||
[2026-02-06 08:00:10] OK: D406
|
||||
[2026-02-06 08:00:11] OK: BILANT_2025
|
||||
[2026-02-06 08:00:11] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-06 08:00:11] OK: SIT_FIN_AN_2025
|
||||
[2026-02-06 08:00:11] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-06 08:00:11] === Monitor complete ===
|
||||
[2026-02-06 14:00:35] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-06 14:00:35] OK: D100
|
||||
[2026-02-06 14:00:35] OK: D101
|
||||
[2026-02-06 14:00:35] OK: D300
|
||||
[2026-02-06 14:00:35] OK: D390
|
||||
[2026-02-06 14:00:35] OK: D394
|
||||
[2026-02-06 14:00:36] OK: D205
|
||||
[2026-02-06 14:00:36] OK: D406
|
||||
[2026-02-06 14:00:36] OK: BILANT_2025
|
||||
[2026-02-06 14:00:36] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-06 14:00:36] OK: SIT_FIN_AN_2025
|
||||
[2026-02-06 14:00:36] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-06 14:00:36] === Monitor complete ===
|
||||
[2026-02-09 08:00:18] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-09 08:00:19] OK: D100
|
||||
[2026-02-09 08:00:19] OK: D101
|
||||
[2026-02-09 08:00:19] OK: D300
|
||||
[2026-02-09 08:00:19] OK: D390
|
||||
[2026-02-09 08:00:19] OK: D394
|
||||
[2026-02-09 08:00:19] OK: D205
|
||||
[2026-02-09 08:00:19] OK: D406
|
||||
[2026-02-09 08:00:19] OK: BILANT_2025
|
||||
[2026-02-09 08:00:20] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-09 08:00:20] OK: SIT_FIN_AN_2025
|
||||
[2026-02-09 08:00:20] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-09 08:00:20] === Monitor complete ===
|
||||
[2026-02-09 14:00:20] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-09 14:00:20] CHANGES in D100: ['Soft A: 02.02.2026 → 09.02.2026']
|
||||
[2026-02-09 14:00:20] OK: D101
|
||||
[2026-02-09 14:00:20] OK: D300
|
||||
[2026-02-09 14:00:20] OK: D390
|
||||
[2026-02-09 14:00:20] OK: D394
|
||||
[2026-02-09 14:00:21] OK: D205
|
||||
[2026-02-09 14:00:21] OK: D406
|
||||
[2026-02-09 14:00:21] OK: BILANT_2025
|
||||
[2026-02-09 14:00:21] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-09 14:00:21] OK: SIT_FIN_AN_2025
|
||||
[2026-02-09 14:00:21] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-09 14:00:21] === Monitor complete ===
|
||||
[2026-02-10 08:00:17] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-10 08:00:17] OK: D100
|
||||
[2026-02-10 08:00:17] OK: D101
|
||||
[2026-02-10 08:00:17] OK: D300
|
||||
[2026-02-10 08:00:17] OK: D390
|
||||
[2026-02-10 08:00:17] OK: D394
|
||||
[2026-02-10 08:00:17] OK: D205
|
||||
[2026-02-10 08:00:17] OK: D406
|
||||
[2026-02-10 08:00:18] OK: BILANT_2025
|
||||
[2026-02-10 08:00:18] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-10 08:00:18] OK: SIT_FIN_AN_2025
|
||||
[2026-02-10 08:00:18] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-10 08:00:18] === Monitor complete ===
|
||||
[2026-02-10 12:39:04] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-10 12:39:04] CHANGES in D100: ['Soft A: 09.02.2026 → 10.02.2026']
|
||||
[2026-02-10 12:39:04] OK: D101
|
||||
[2026-02-10 12:39:04] OK: D300
|
||||
[2026-02-10 12:39:05] OK: D390
|
||||
[2026-02-10 12:39:05] OK: D394
|
||||
[2026-02-10 12:39:05] OK: D205
|
||||
[2026-02-10 12:39:05] OK: D406
|
||||
[2026-02-10 12:39:05] OK: BILANT_2025
|
||||
[2026-02-10 12:39:05] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-10 12:39:06] OK: SIT_FIN_AN_2025
|
||||
[2026-02-10 12:39:06] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-10 12:39:06] === Monitor complete ===
|
||||
[2026-02-10 14:00:25] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-10 14:00:25] OK: D100
|
||||
[2026-02-10 14:00:25] OK: D101
|
||||
[2026-02-10 14:00:25] OK: D300
|
||||
[2026-02-10 14:00:25] OK: D390
|
||||
[2026-02-10 14:00:25] OK: D394
|
||||
[2026-02-10 14:00:25] OK: D205
|
||||
[2026-02-10 14:00:25] OK: D406
|
||||
[2026-02-10 14:00:25] OK: BILANT_2025
|
||||
[2026-02-10 14:00:25] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-10 14:00:26] OK: SIT_FIN_AN_2025
|
||||
[2026-02-10 14:00:26] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-10 14:00:26] === Monitor complete ===
|
||||
[2026-02-11 08:00:21] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-11 08:00:21] OK: D100
|
||||
[2026-02-11 08:00:21] OK: D101
|
||||
[2026-02-11 08:00:21] OK: D300
|
||||
[2026-02-11 08:00:21] OK: D390
|
||||
[2026-02-11 08:00:22] OK: D394
|
||||
[2026-02-11 08:00:22] OK: D205
|
||||
[2026-02-11 08:00:22] OK: D406
|
||||
[2026-02-11 08:00:22] CHANGES in BILANT_2025: ['Soft J S1003: 04.02.2025 → 10.02.2026']
|
||||
[2026-02-11 08:00:22] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-11 08:00:22] OK: SIT_FIN_AN_2025
|
||||
[2026-02-11 08:00:22] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-11 08:00:22] === Monitor complete ===
|
||||
[2026-02-11 14:00:17] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-11 14:00:17] OK: D100
|
||||
[2026-02-11 14:00:17] OK: D101
|
||||
[2026-02-11 14:00:17] CHANGES in D300: ['Soft A: 16.12.2025 → 11.02.2026', 'Soft J: 10.09.2025 → 11.02.2026']
|
||||
[2026-02-11 14:00:17] OK: D390
|
||||
[2026-02-11 14:00:17] OK: D394
|
||||
[2026-02-11 14:00:17] OK: D205
|
||||
[2026-02-11 14:00:18] CHANGES in D406: ['Soft J: 30.10.2025 → 11.02.2026']
|
||||
[2026-02-11 14:00:18] CHANGES in BILANT_2025: ['Soft A: 27.01.2026 → 11.02.2026']
|
||||
[2026-02-11 14:00:18] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-11 14:00:18] OK: SIT_FIN_AN_2025
|
||||
[2026-02-11 14:00:18] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-11 14:00:18] === Monitor complete ===
|
||||
[2026-02-11 16:23:14] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-11 16:23:14] OK: D100
|
||||
[2026-02-11 16:23:14] OK: D101
|
||||
[2026-02-11 16:23:15] HASH CHANGED in D300 (no version changes detected)
|
||||
[2026-02-11 16:23:15] OK: D390
|
||||
[2026-02-11 16:23:15] OK: D394
|
||||
[2026-02-11 16:23:15] OK: D205
|
||||
[2026-02-11 16:23:15] OK: D406
|
||||
[2026-02-11 16:23:15] OK: BILANT_2025
|
||||
[2026-02-11 16:23:15] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-11 16:23:15] OK: SIT_FIN_AN_2025
|
||||
[2026-02-11 16:23:15] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-11 16:23:15] === Monitor complete ===
|
||||
[2026-02-12 08:00:23] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-12 08:00:24] OK: D100
|
||||
[2026-02-12 08:00:24] OK: D101
|
||||
[2026-02-12 08:00:24] OK: D300
|
||||
[2026-02-12 08:00:24] OK: D390
|
||||
[2026-02-12 08:00:24] OK: D394
|
||||
[2026-02-12 08:00:25] OK: D205
|
||||
[2026-02-12 08:00:25] OK: D406
|
||||
[2026-02-12 08:00:28] OK: BILANT_2025
|
||||
[2026-02-12 08:00:28] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-12 08:00:28] OK: SIT_FIN_AN_2025
|
||||
[2026-02-12 08:00:28] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-12 08:00:28] === Monitor complete ===
|
||||
[2026-02-12 14:00:22] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-12 14:00:22] OK: D100
|
||||
[2026-02-12 14:00:22] OK: D101
|
||||
[2026-02-12 14:00:22] CHANGES in D300: ['Soft A: 11.02.2026 → 12.02.2026']
|
||||
[2026-02-12 14:00:22] OK: D390
|
||||
[2026-02-12 14:00:22] OK: D394
|
||||
[2026-02-12 14:00:22] OK: D205
|
||||
[2026-02-12 14:00:23] OK: D406
|
||||
[2026-02-12 14:00:23] HASH CHANGED in BILANT_2025 (no version changes detected)
|
||||
[2026-02-12 14:00:23] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-12 14:00:23] HASH CHANGED in SIT_FIN_AN_2025 (no version changes detected)
|
||||
[2026-02-12 14:00:23] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-12 14:00:23] === Monitor complete ===
|
||||
[2026-02-12 14:46:11] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-12 14:46:11] OK: D100
|
||||
[2026-02-12 14:46:11] OK: D101
|
||||
[2026-02-12 14:46:11] OK: D300
|
||||
[2026-02-12 14:46:11] OK: D390
|
||||
[2026-02-12 14:46:11] OK: D394
|
||||
[2026-02-12 14:46:11] OK: D205
|
||||
[2026-02-12 14:46:12] OK: D406
|
||||
[2026-02-12 14:46:12] OK: BILANT_2025
|
||||
[2026-02-12 14:46:12] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-12 14:46:12] OK: SIT_FIN_AN_2025
|
||||
[2026-02-12 14:46:12] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-12 14:46:12] === Monitor complete ===
|
||||
[2026-02-13 08:00:14] === Starting ANAF monitor v2.1 ===
|
||||
[2026-02-13 08:00:14] OK: D100
|
||||
[2026-02-13 08:00:14] OK: D101
|
||||
[2026-02-13 08:00:14] OK: D300
|
||||
[2026-02-13 08:00:14] OK: D390
|
||||
[2026-02-13 08:00:15] OK: D394
|
||||
[2026-02-13 08:00:16] OK: D205
|
||||
[2026-02-13 08:00:16] OK: D406
|
||||
[2026-02-13 08:00:16] HASH CHANGED in BILANT_2025 (no version changes detected)
|
||||
[2026-02-13 08:00:16] OK: SIT_FIN_SEM_2025
|
||||
[2026-02-13 08:00:16] HASH CHANGED in SIT_FIN_AN_2025 (no version changes detected)
|
||||
[2026-02-13 08:00:16] OK: DESCARCARE_DECLARATII
|
||||
[2026-02-13 08:00:16] === Monitor complete ===
|
||||
372
tools/anaf-monitor/monitor_v2.py
Normal file
372
tools/anaf-monitor/monitor_v2.py
Normal file
@@ -0,0 +1,372 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ANAF Monitor v2.2 - Hash detection + version extraction + text diff
|
||||
- Hash-based change detection (catches ANY change)
|
||||
- Extracts ALL soft A/J versions from page
|
||||
- Saves page text and shows diff on changes
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import hashlib
|
||||
import urllib.request
|
||||
import ssl
|
||||
import difflib
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from html.parser import HTMLParser
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||
VERSIONS_FILE = SCRIPT_DIR / "versions.json"
|
||||
HASHES_FILE = SCRIPT_DIR / "hashes.json"
|
||||
SNAPSHOTS_DIR = SCRIPT_DIR / "snapshots"
|
||||
LOG_FILE = SCRIPT_DIR / "monitor.log"
|
||||
DASHBOARD_STATUS = SCRIPT_DIR.parent.parent / "dashboard" / "status.json"
|
||||
|
||||
# Ensure snapshots directory exists
|
||||
SNAPSHOTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
class TextExtractor(HTMLParser):
|
||||
"""Extract visible text from HTML"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.text = []
|
||||
self.skip_tags = {'script', 'style', 'head', 'meta', 'link'}
|
||||
self.current_tag = None
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
self.current_tag = tag.lower()
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
self.current_tag = None
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.current_tag not in self.skip_tags:
|
||||
text = data.strip()
|
||||
if text:
|
||||
self.text.append(text)
|
||||
|
||||
def get_text(self):
|
||||
return '\n'.join(self.text)
|
||||
|
||||
|
||||
def html_to_text(html):
|
||||
"""Convert HTML to plain text"""
|
||||
parser = TextExtractor()
|
||||
try:
|
||||
parser.feed(html)
|
||||
return parser.get_text()
|
||||
except:
|
||||
# Fallback: just strip tags
|
||||
return re.sub(r'<[^>]+>', ' ', html)
|
||||
|
||||
SSL_CTX = ssl.create_default_context()
|
||||
SSL_CTX.check_hostname = False
|
||||
SSL_CTX.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def log(msg):
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(f"[{timestamp}] {msg}\n")
|
||||
|
||||
def load_json(path, default=None):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return default if default is not None else {}
|
||||
|
||||
def save_json(path, data):
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def fetch_page(url, timeout=30):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; ANAF-Monitor/2.1)'
|
||||
})
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=SSL_CTX) as resp:
|
||||
return resp.read()
|
||||
except Exception as e:
|
||||
log(f"ERROR fetching {url}: {e}")
|
||||
return None
|
||||
|
||||
def compute_hash(content):
|
||||
"""Compute SHA256 hash of content"""
|
||||
return hashlib.sha256(content).hexdigest()
|
||||
|
||||
|
||||
def load_snapshot(page_id):
|
||||
"""Load previous page text snapshot"""
|
||||
snapshot_file = SNAPSHOTS_DIR / f"{page_id}.txt"
|
||||
try:
|
||||
return snapshot_file.read_text(encoding='utf-8')
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def save_snapshot(page_id, text):
|
||||
"""Save page text snapshot"""
|
||||
snapshot_file = SNAPSHOTS_DIR / f"{page_id}.txt"
|
||||
snapshot_file.write_text(text, encoding='utf-8')
|
||||
|
||||
|
||||
def generate_diff(old_text, new_text, context_lines=3):
|
||||
"""Generate unified diff between old and new text"""
|
||||
if not old_text:
|
||||
return None
|
||||
|
||||
old_lines = old_text.splitlines(keepends=True)
|
||||
new_lines = new_text.splitlines(keepends=True)
|
||||
|
||||
diff = list(difflib.unified_diff(
|
||||
old_lines, new_lines,
|
||||
fromfile='anterior',
|
||||
tofile='actual',
|
||||
n=context_lines
|
||||
))
|
||||
|
||||
if not diff:
|
||||
return None
|
||||
|
||||
# Limitează diff-ul la maxim 50 linii pentru output
|
||||
if len(diff) > 50:
|
||||
diff = diff[:50] + ['... (truncat)\n']
|
||||
|
||||
return ''.join(diff)
|
||||
|
||||
def parse_date_from_filename(filename):
|
||||
"""Extrage data din numele fișierului (ex: D394_26092025.pdf -> 26.09.2025)"""
|
||||
# Pattern: _DDMMYYYY. sau _DDMMYYYY_ sau _YYYYMMDD
|
||||
match = re.search(r'_(\d{8})[\._]', filename)
|
||||
if match:
|
||||
d = match.group(1)
|
||||
# Verifică dacă e DDMMYYYY sau YYYYMMDD
|
||||
if int(d[:2]) <= 31 and int(d[2:4]) <= 12:
|
||||
return f"{d[:2]}.{d[2:4]}.{d[4:]}"
|
||||
elif int(d[4:6]) <= 12 and int(d[6:]) <= 31:
|
||||
return f"{d[6:]}.{d[4:6]}.{d[:4]}"
|
||||
|
||||
# Pattern: _DDMMYY
|
||||
match = re.search(r'_(\d{6})[\._]', filename)
|
||||
if match:
|
||||
d = match.group(1)
|
||||
if int(d[:2]) <= 31 and int(d[2:4]) <= 12:
|
||||
return f"{d[:2]}.{d[2:4]}.20{d[4:]}"
|
||||
|
||||
return None
|
||||
|
||||
def extract_versions(html):
|
||||
"""Extrage soft A/J din HTML - primul generic + toate cele cu label (S1002, etc.)"""
|
||||
versions = {}
|
||||
|
||||
# Găsește PRIMUL link soft A (PDF) - versiunea curentă
|
||||
soft_a_match = re.search(
|
||||
r'<a[^>]+href=["\']([^"\']*\.pdf)["\'][^>]*>\s*soft\s*A\s*</a>',
|
||||
html, re.IGNORECASE
|
||||
)
|
||||
if soft_a_match:
|
||||
url = soft_a_match.group(1)
|
||||
versions['soft_a_url'] = url
|
||||
date = parse_date_from_filename(url)
|
||||
if date:
|
||||
versions['soft_a_date'] = date
|
||||
|
||||
# Găsește soft J-uri CU LABEL (ex: "soft J - S1002") - toate
|
||||
soft_j_labeled = re.findall(
|
||||
r'<a[^>]+href=["\']([^"\']*\.zip)["\'][^>]*>\s*soft\s*J\s*-\s*([^<]+)',
|
||||
html, re.IGNORECASE
|
||||
)
|
||||
|
||||
if soft_j_labeled:
|
||||
# Pagină cu soft-uri denumite (bilanț)
|
||||
for url, label in soft_j_labeled:
|
||||
label = label.strip()
|
||||
key = f'soft_j_{label.replace(" ", "_")}'
|
||||
versions[f'{key}_url'] = url
|
||||
date = parse_date_from_filename(url)
|
||||
if date:
|
||||
versions[f'{key}_date'] = date
|
||||
else:
|
||||
# Pagină cu soft J simplu - ia doar primul
|
||||
soft_j_match = re.search(
|
||||
r'<a[^>]+href=["\']([^"\']*\.zip)["\'][^>]*>\s*soft\s*J',
|
||||
html, re.IGNORECASE
|
||||
)
|
||||
if soft_j_match:
|
||||
url = soft_j_match.group(1)
|
||||
versions['soft_j_url'] = url
|
||||
date = parse_date_from_filename(url)
|
||||
if date:
|
||||
versions['soft_j_date'] = date
|
||||
|
||||
# Găsește data publicării din text
|
||||
publish_match = re.search(
|
||||
r'publicat\s+[îi]n\s*(?:data\s+de\s*)?(\d{2}[./]\d{2}[./]\d{4})',
|
||||
html, re.IGNORECASE
|
||||
)
|
||||
if publish_match:
|
||||
versions['published'] = publish_match.group(1).replace('/', '.')
|
||||
|
||||
return versions
|
||||
|
||||
def compare_versions(old, new):
|
||||
"""Compară versiunile și returnează diferențele"""
|
||||
changes = []
|
||||
|
||||
# Colectează toate cheile unice
|
||||
all_keys = set(old.keys()) | set(new.keys())
|
||||
date_keys = sorted([k for k in all_keys if k.endswith('_date') or k == 'published'])
|
||||
|
||||
for key in date_keys:
|
||||
old_val = old.get(key)
|
||||
new_val = new.get(key)
|
||||
|
||||
# Formatează label-ul
|
||||
label = key.replace('_date', '').replace('_', ' ').title()
|
||||
|
||||
if new_val and old_val != new_val:
|
||||
if old_val:
|
||||
changes.append(f"{label}: {old_val} → {new_val}")
|
||||
else:
|
||||
changes.append(f"{label}: {new_val} (NOU)")
|
||||
|
||||
return changes
|
||||
|
||||
def format_current_versions(versions):
|
||||
"""Formatează versiunile curente pentru output"""
|
||||
result = {}
|
||||
for key, val in versions.items():
|
||||
if key.endswith('_date'):
|
||||
label = key.replace('_date', '')
|
||||
result[label] = val
|
||||
return result
|
||||
|
||||
def check_page(page, saved_versions, saved_hashes):
|
||||
"""Verifică o pagină și returnează modificările"""
|
||||
page_id = page["id"]
|
||||
name = page["name"]
|
||||
url = page["url"]
|
||||
|
||||
content = fetch_page(url)
|
||||
if content is None:
|
||||
return None
|
||||
|
||||
# 1. Verifică hash-ul mai întâi (detectează ORICE schimbare)
|
||||
new_hash = compute_hash(content)
|
||||
old_hash = saved_hashes.get(page_id)
|
||||
|
||||
html = content.decode('utf-8', errors='ignore')
|
||||
new_text = html_to_text(html)
|
||||
new_versions = extract_versions(html)
|
||||
old_versions = saved_versions.get(page_id, {})
|
||||
|
||||
# Încarcă snapshot-ul anterior
|
||||
old_text = load_snapshot(page_id)
|
||||
|
||||
# Prima rulare - inițializare
|
||||
if not old_hash:
|
||||
log(f"INIT: {page_id}")
|
||||
saved_hashes[page_id] = new_hash
|
||||
saved_versions[page_id] = new_versions
|
||||
save_snapshot(page_id, new_text)
|
||||
return None
|
||||
|
||||
# Compară hash-uri
|
||||
hash_changed = new_hash != old_hash
|
||||
|
||||
# Compară versiuni pentru detalii
|
||||
version_changes = compare_versions(old_versions, new_versions)
|
||||
|
||||
# Generează diff dacă s-a schimbat
|
||||
diff = None
|
||||
if hash_changed and old_text:
|
||||
diff = generate_diff(old_text, new_text)
|
||||
|
||||
# Actualizează starea
|
||||
saved_hashes[page_id] = new_hash
|
||||
saved_versions[page_id] = new_versions
|
||||
save_snapshot(page_id, new_text)
|
||||
|
||||
if hash_changed:
|
||||
if version_changes:
|
||||
log(f"CHANGES in {page_id}: {version_changes}")
|
||||
else:
|
||||
log(f"HASH CHANGED in {page_id} (no version changes detected)")
|
||||
version_changes = ["Pagina s-a modificat"]
|
||||
|
||||
result = {
|
||||
"id": page_id,
|
||||
"name": name,
|
||||
"url": url,
|
||||
"changes": version_changes,
|
||||
"current": format_current_versions(new_versions)
|
||||
}
|
||||
|
||||
if diff:
|
||||
result["diff"] = diff
|
||||
|
||||
return result
|
||||
|
||||
log(f"OK: {page_id}")
|
||||
return None
|
||||
|
||||
def update_dashboard_status(has_changes, changes_count, changes_list=None):
|
||||
"""Actualizează status.json pentru dashboard"""
|
||||
try:
|
||||
status = load_json(DASHBOARD_STATUS, {})
|
||||
anaf_status = {
|
||||
'ok': not has_changes,
|
||||
'status': 'MODIFICĂRI' if has_changes else 'OK',
|
||||
'message': f'{changes_count} modificări detectate' if has_changes else 'Nicio modificare detectată',
|
||||
'lastCheck': datetime.now().strftime('%d %b %Y, %H:%M'),
|
||||
'changesCount': changes_count
|
||||
}
|
||||
|
||||
# Adaugă detaliile modificărilor pentru dashboard
|
||||
if has_changes and changes_list:
|
||||
anaf_status['changes'] = []
|
||||
for change in changes_list:
|
||||
change_detail = {
|
||||
'name': change.get('name', ''),
|
||||
'url': change.get('url', ''),
|
||||
'summary': []
|
||||
}
|
||||
# Ia primele 3 modificări ca rezumat
|
||||
if change.get('changes'):
|
||||
change_detail['summary'] = change['changes'][:3]
|
||||
anaf_status['changes'].append(change_detail)
|
||||
|
||||
status['anaf'] = anaf_status
|
||||
save_json(DASHBOARD_STATUS, status)
|
||||
except Exception as e:
|
||||
log(f"ERROR updating dashboard status: {e}")
|
||||
|
||||
def main():
|
||||
log("=== Starting ANAF monitor v2.1 ===")
|
||||
|
||||
config = load_json(CONFIG_FILE, {"pages": []})
|
||||
saved_versions = load_json(VERSIONS_FILE, {})
|
||||
saved_hashes = load_json(HASHES_FILE, {})
|
||||
|
||||
all_changes = []
|
||||
for page in config["pages"]:
|
||||
result = check_page(page, saved_versions, saved_hashes)
|
||||
if result:
|
||||
all_changes.append(result)
|
||||
|
||||
save_json(VERSIONS_FILE, saved_versions)
|
||||
save_json(HASHES_FILE, saved_hashes)
|
||||
|
||||
# Update dashboard status
|
||||
update_dashboard_status(len(all_changes) > 0, len(all_changes), all_changes)
|
||||
|
||||
log("=== Monitor complete ===")
|
||||
|
||||
print(json.dumps({"changes": all_changes}, ensure_ascii=False, indent=2))
|
||||
return len(all_changes)
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
40
tools/anaf-monitor/snapshots/BILANT_2025.txt
Normal file
40
tools/anaf-monitor/snapshots/BILANT_2025.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
S1002-S1003-S1004-S1005
|
||||
S1002-S1003-S1004-S1005
|
||||
S1011
|
||||
S1019
|
||||
S1025
|
||||
S1030
|
||||
S1039
|
||||
S1040-S1041
|
||||
S1042
|
||||
S1046
|
||||
S1051-S1052-S1053-S1054
|
||||
Tabel
|
||||
codificări
|
||||
tipuri de situaţii financiare şi raportări anuale
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
Atenție! Momentan se pot depune doar S1002,S1003 şi S1005.
|
||||
S1002-S1005
|
||||
Situaţii financiare anuale la
|
||||
31 decembrie 2025
|
||||
întocmite de către entităţile al căror exerciţiu financiar coincide cu anul calendaristic, prevăzute la pct. 1.1 din Anexa 1 la
|
||||
OMF nr. 2036/23.12.2025
|
||||
privind principalele aspecte legate de întocmirea şi depunerea situaţiilor financiare anuale şi a raportărilor contabile anuale ale operatorilor economici la Agentia Nationala de Administrare Fiscala. Potrivit art. 185 din Legea societăţilor nr. 31/1990, republicată, cu modificările şi completările ulterioare, consiliul de administraţie, respectiv directoratul, este obligat să depună la ANAF numai în format electronic pe portalul www.e-guvernare.ro, având ataşată o semnătură electronică extinsă, situaţiile financiare anuale, raportul lor, raportul cenzorilor sau raportul auditorilor financiari, după caz. Formatul electronic al situaţiilor financiare anuale la 31 decembrie 2023, generat prin programele de asistenţă, constă într-un fişier PDF având ataşat un fişier xml (care conţine formularele cod 10, cod 20, cod 30 şi cod 40), la care trebuie ataşat şi un fişier cu extensia zip.
|
||||
Fişierul cu extensia zip va conţine prima pagină din situaţiile financiare anuale listată cu ajutorul programului de asistenţă elaborat de Ministerul Finanţelor, semnată, potrivit legii, precum şi documentele cerute de lege (de exemplu: note explicative la situaţiile financiare anuale, situaţia modificărilor capitalului propriu şi situaţia fluxurilor de numerar, după caz; raportul administratorilor; raportul de audit sau raportul comisiei de cenzori, după caz; propunerea de distribuire a profitului sau de acoperire a pierderii contabile; declaraţia scrisă a persoanelor prevăzute la art. 10 alin. (1) din legea contabilităţii, prin care îşi asumă răspunderea pentru întocmirea situaţiilor financiare anuale), aşa cum acestea sunt întocmite de entităţi, toate acestea fiind scanate, alb-negru, lizibil şi cu o rezoluţie care să permită încadrarea în limita a 9,5 MB a fişierului PDF la care este ataşat fişierul zip. Fişierele zip ataşate situaţiilor financiare anuale nu vor conţine parolă.
|
||||
- publicat în data de
|
||||
28.01.2026
|
||||
soft A
|
||||
actualizat în data
|
||||
11.02.2026
|
||||
soft J - S1002
|
||||
soft J - S1003
|
||||
soft J - S1005
|
||||
Schema XSD 1002
|
||||
Schema XSD 1003
|
||||
Schema XSD 1005
|
||||
Structura
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
263
tools/anaf-monitor/snapshots/D100.txt
Normal file
263
tools/anaf-monitor/snapshots/D100.txt
Normal file
@@ -0,0 +1,263 @@
|
||||
|
||||
100
|
||||
017
|
||||
060
|
||||
093
|
||||
100
|
||||
101
|
||||
101 G
|
||||
104
|
||||
106
|
||||
107
|
||||
108
|
||||
109
|
||||
110
|
||||
112
|
||||
114
|
||||
119
|
||||
120
|
||||
130
|
||||
177
|
||||
179
|
||||
180
|
||||
182
|
||||
205
|
||||
207
|
||||
216
|
||||
217
|
||||
222
|
||||
300
|
||||
301
|
||||
307
|
||||
311
|
||||
390
|
||||
392
|
||||
393
|
||||
394
|
||||
395
|
||||
397
|
||||
398
|
||||
401
|
||||
402
|
||||
403
|
||||
404
|
||||
405
|
||||
406
|
||||
407
|
||||
700
|
||||
708
|
||||
710
|
||||
711
|
||||
900
|
||||
901
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform
|
||||
OPANAF nr. 57/19.01.2026,
|
||||
valabil începand cu
|
||||
01/2024 - publicat în data de 09.02.2024
|
||||
soft A
|
||||
actualizat în data de
|
||||
10.02.2026
|
||||
soft J*
|
||||
actualizat în data de
|
||||
23.01.2026
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
09.02.2026
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF nr. 64/18.01.2022, OPANAF
|
||||
nr. 237/18.02.2022, OPANAF nr. 449/23.03.2022,
|
||||
OPANAF nr. 1150/ 2022, OPANAF nr. 1341/2022, OPANAF nr. 1542/23.08.2022,
|
||||
OPANAF nr. 1635/12.09.2022
|
||||
, OPANAF nr. 172/08.02.2023,OPANAF nr. 188/10.02.2023,
|
||||
OPANAF nr. 1090/07.07.2023,OPANAF nr. 1857/06.11.2023
|
||||
valabil începand cu
|
||||
12/2021
|
||||
- publicat în data de
|
||||
24.02.2022
|
||||
soft A
|
||||
actualizat în data de
|
||||
25.01.2024
|
||||
soft J*
|
||||
actualizat în data de
|
||||
17.01.2024
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
21.11.2023
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF nr. 51/18.01.2021, valabil începand cu 12/2020 - publicat în data de
|
||||
20.01.2021
|
||||
Formularul nu conţine modificările conform OUG 153/2020.
|
||||
Termenul legal de depunere
|
||||
pentru impozitul pe veniturile microîntreprinderilor - trim. IV 2021 şi impozitul specific - sem. II 2021
|
||||
este 25 iunie 2022.
|
||||
soft A
|
||||
actualizat în data de
|
||||
20.01.2022
|
||||
soft J*
|
||||
actualizat în data de
|
||||
20.01.2022
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
20.01.2022
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF
|
||||
nr. 935/ 14.04.2020
|
||||
, valabil începand cu
|
||||
09/2020
|
||||
- publicat în data de
|
||||
30.09.2020
|
||||
soft A
|
||||
actualizat în data de
|
||||
23.10.2020
|
||||
soft J*
|
||||
actualizat în data de
|
||||
30.09.2020
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
30.09.2020
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF
|
||||
nr. 935/ 14.04.2020
|
||||
, valabil începand cu
|
||||
03/2020
|
||||
- publicat în data de
|
||||
22.04.2020
|
||||
soft A
|
||||
actualizat în data de
|
||||
30.09.2020
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF nr. 1203/ 10.05.2018, valabil începand cu 05/2018 - publicat în data de
|
||||
07.06.2018
|
||||
soft A
|
||||
actualizat în data de
|
||||
05.12.2019
|
||||
soft J*
|
||||
actualizat în data de
|
||||
28.01.2020
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
21.02.2019
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile
|
||||
de plată la bugetul de stat, conform OPANAF 3781/ 22.12.2017, -
|
||||
publicat în data de
|
||||
01.02.2018
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile
|
||||
de plată la bugetul de stat, conform OPANAF 2935/ 11.10.2017, valabil începand cu 09/ 2017 -
|
||||
publicat în data de
|
||||
17.10.2017
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF 869/ 08.03.2017, valabil începand cu 02/ 2017 -
|
||||
publicat în data de
|
||||
16.03.2017
|
||||
soft A
|
||||
actualizat în data de
|
||||
01.08.2017
|
||||
soft J*
|
||||
actualizat în data de
|
||||
01.08.2017
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
01.08.2017
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF 587/2016
|
||||
cu modificările şi completările ulterioare, valabil începand cu 01/2016 -
|
||||
publicat în data de
|
||||
07.12.2016 - versiune bilingvă română - engleză
|
||||
soft A
|
||||
soft J*
|
||||
actualizat în data de
|
||||
21.03.2016
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
21.03.2016
|
||||
Schema
|
||||
XSD
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF 587/2016
|
||||
cu modificările şi completările ulterioare, valabil începand cu 01/2016 -
|
||||
publicat în data de
|
||||
15.02.2016
|
||||
soft A
|
||||
actualizat în data de
|
||||
15.04.2016
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF 123/ 29.01.2014 valabil incepand cu 01/2014 - actualizat în data de
|
||||
19.01.2015
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D100
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF 3136/ 26.09.2013 - actualizat în
|
||||
20.01.2014
|
||||
soft A
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D100
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF 1135/ 30.07.2012 pentru an 2012 - actualizat în
|
||||
28.12.2012
|
||||
soft A
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D100
|
||||
100
|
||||
- Declaraţie privind obligaţiile de plată la bugetul de stat, conform OPANAF 1932/2011, utilizată începând cu declararea obligaţiilor fiscale aferente lunii noiembrie 2011 - actualizat în
|
||||
12.01.2012
|
||||
soft A
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D100
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
196
tools/anaf-monitor/snapshots/D101.txt
Normal file
196
tools/anaf-monitor/snapshots/D101.txt
Normal file
@@ -0,0 +1,196 @@
|
||||
101
|
||||
Ascultă
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
101
|
||||
- Declaraţie privind impozitul pe
|
||||
profit conform
|
||||
OPANAF nr. 206/11.02.2025.
|
||||
Valabil pentru an sfârşit de perioada >= 2024
|
||||
publicat în 14.02.2025
|
||||
.
|
||||
soft A
|
||||
actualizat în data de
|
||||
26.01.2026
|
||||
soft J*
|
||||
actualizat în data de
|
||||
23.01.2026
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
26.01.2026
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
14.02.2025
|
||||
101
|
||||
- Declaraţie privind impozitul pe
|
||||
profit conform
|
||||
OPANAF nr. 310/07.03.2023,
|
||||
OPANAF nr. 423/17.03.2022. Valabil pentru an sfârşit de perioada >= 2022
|
||||
publicat în 22.03.2022
|
||||
.
|
||||
soft A
|
||||
actualizat în data de
|
||||
04.03.2024
|
||||
soft J*
|
||||
actualizat în data de
|
||||
24.04.2023
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
24.04.2023
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
24.04.2023
|
||||
101
|
||||
- Declaraţie privind impozitul pe
|
||||
profit conform OPANAF nr. 423/ 17.03.2020, publicat în MO nr.1246/
|
||||
17.12.2020. Valabil pentru an sfârşit de perioada >= 2021
|
||||
publicat în 29.03.2022
|
||||
.
|
||||
soft A
|
||||
actualizat în data de
|
||||
10.10.2022
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
101
|
||||
- Declaraţie privind impozitul pe
|
||||
profit conform OPANAF nr. 4072/ 09.12.2020, publicat în MO nr.1246/
|
||||
17.12.2020. Valabil pentru an sfârşit de perioada >= 2020
|
||||
publicat în 12.01.2021
|
||||
.
|
||||
soft A
|
||||
actualizat în data de
|
||||
15.02.2021
|
||||
soft J*
|
||||
actualizat în data de
|
||||
03.02.2021
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
101
|
||||
- Declaraţie privind impozitul pe profit conform OPANAF nr.
|
||||
3200/20.12.2018.
|
||||
Valabil pentru an sfârşit de perioada >= 2018 -
|
||||
publicat în 21.01.2019
|
||||
.
|
||||
soft A
|
||||
actualizat în data de
|
||||
21.06.2019
|
||||
soft J*
|
||||
actualizat în data de
|
||||
25.04.2019
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
12.02.2019
|
||||
Schema
|
||||
XSD
|
||||
101
|
||||
- Declaraţie privind impozitul pe profit conform OPANAF nr.4160/ 28.12.2017.
|
||||
Valabil pentru an sfârşit de perioada >= 2017 -
|
||||
publicat în 14.02.2018
|
||||
.
|
||||
soft A
|
||||
actualizat în data de
|
||||
27.07.2018
|
||||
soft J*
|
||||
actualizat în data de
|
||||
27.07.2018
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
27.07.2018
|
||||
Schema
|
||||
XSD
|
||||
101
|
||||
- Declaraţie privind impozitul pe
|
||||
profit conform OPANAF nr.3386/2016, publicat în M.O. nr.950/25.11.2016
|
||||
Ca urmare a modificării plafonului de venituri, vor depune D101
|
||||
la 01/2017 respectiv 07/2017, persoanele juridice romane care la data de
|
||||
31.12.2016 au realizat venituri cuprinse între
|
||||
100001-500000
|
||||
euro,inclusiv, si care :
|
||||
- sunt plătitoare de impozit pe profit la
|
||||
data de 31.01.2017 si vor intra în categoria microintreprinderilor
|
||||
încep<65>nd cu 01.02.2017 (scadenta 25.02.2017)
|
||||
- sunt plătitoare de
|
||||
impozit pe profit la data de 31.07.2017 si vor intra în categoria
|
||||
microintreprinderilor încep<65>nd cu 01.08.2017 (scadenta 25.08.2017)
|
||||
Valabil pentru lunile ianuarie 2017 si iulie 2017 - actualizat în
|
||||
data de
|
||||
31.07.2017
|
||||
soft A
|
||||
actualizat în data de
|
||||
21.11.2017
|
||||
soft J*
|
||||
actualizat în data de
|
||||
21.11.2017
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
31.07.2017
|
||||
Schema
|
||||
XSD
|
||||
101
|
||||
- Declaraţie privind impozitul pe profit,
|
||||
conform OPANAF nr. 3386/ 25.11.2016 publicat în M.O. nr.950/25.11.2016 - valabil începând cu anul 2016 - publicat în data de
|
||||
27.01.2017
|
||||
soft A
|
||||
actualizat în data de
|
||||
08.02.2017
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
08.02.2017
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
06.02.2017
|
||||
101
|
||||
- Declaraţie privind impozitul pe profit, conform OPANAF nr. 3250/ 2015 (M.OF. nr.905/ 07.12.2015) - valabil încep<65>nd cu anul 2015 - publicat
|
||||
07.12.2016 versiune bilingvă română - engleză
|
||||
soft A
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
101
|
||||
- Declaraţie privind impozitul pe profit, conform OPANAF nr. 3250/ 2015 (M.OF. nr.905/ 07.12.2015) - valabil încep<65>nd cu anul 2015 - publicat
|
||||
11.01.2016
|
||||
soft A
|
||||
actualizat în data de
|
||||
07.12.2016
|
||||
101
|
||||
- Declaraţie privind impozitul pe profit, conform OPANAF nr. 4024/ 23.12.2014 (M.OF. nr.2/ 05.01.2015) - valabil începând cu anul 2014 - actualizat
|
||||
23.09.2015
|
||||
soft A
|
||||
actualizat în data de
|
||||
23.09.2015
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
101
|
||||
- Declaraţie privind impozitul pe profit, conform OPANAF nr. 1950/2012 - actualizat în
|
||||
data de 24.01.2014
|
||||
(actualizare pentru persoanele juridice care la 31.12 îndeplinesc condiţiile de plătitor de impozit pe veniturile microintrep. cf. art. 112^2 alin.(3) din L. 571/2003)
|
||||
Pentru perioadele de raportare anterioare anului 2012, declaraţia 101 se poate descărca de pe
|
||||
portalul ANAF
|
||||
secţiunea programe utile
|
||||
şi se pot depune numai la ghişeu.
|
||||
soft A
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
144
tools/anaf-monitor/snapshots/D205.txt
Normal file
144
tools/anaf-monitor/snapshots/D205.txt
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
205
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile
|
||||
din investii, pe beneficiari de venit,
|
||||
conform OPANAF nr. 102/2025( începând cu anul de raportare 2024) - publicat în
|
||||
11.02.2025
|
||||
soft A
|
||||
actualizat în data de
|
||||
15.01.2026
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema XSD
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile
|
||||
din investii, pe beneficiari de venit,
|
||||
conform OPANAF nr. 154/29.01.2024( începând cu anul de raportare 2023) - publicat în
|
||||
05.02.2024
|
||||
soft A
|
||||
actualizat în data de
|
||||
12.04.2024
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema XSD
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile
|
||||
din investii, pe beneficiari de venit,
|
||||
conform OPANAF nr. 18/10.01.2023( începând cu anul de raportare 2022) - publicat în
|
||||
20.01.2023
|
||||
soft A
|
||||
actualizat în data de
|
||||
20.02.2023
|
||||
soft J*
|
||||
actualizat în data de
|
||||
11.12.2023
|
||||
Anexa
|
||||
validări
|
||||
Schema XSD
|
||||
actualizat în data de
|
||||
10.02.2023
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile realizate, pe beneficiari de venit,
|
||||
conform OPANAF ( începând cu anul de raportare 2021) - publicat în
|
||||
09.02.2022
|
||||
soft A
|
||||
soft J*
|
||||
actualizat în data de
|
||||
25.02.2022
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile realizate, pe beneficiari de venit,
|
||||
conform OPANAF 48/11.01.2019 ( începând cu anul de raportare 2018) - publicat în
|
||||
17.01.2019
|
||||
soft A
|
||||
actualizat în data de
|
||||
25.02.2021
|
||||
soft J*
|
||||
actualizat în data de
|
||||
25.02.2021
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
25.02.2021
|
||||
Schema
|
||||
XSD
|
||||
205
|
||||
-Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile realizate, pe beneficiari de venit,
|
||||
conform OPANAF 3726/ 19.12.2017 ( începând cu anul de raportare 2017 ) - publicat în
|
||||
16.01.2018
|
||||
soft A
|
||||
actualizat în data de
|
||||
23.02.2018
|
||||
soft J*
|
||||
actualizat în data de
|
||||
08.02.2018
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
19.02.2018
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
22.02.2018
|
||||
205
|
||||
-Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile realizate, pe beneficiari de venit, conform OPANAF 3695/ 27.12.2016 ( începând cu anul de raportare 2016 ) - publicat în
|
||||
19.01.2017
|
||||
soft A
|
||||
publicat în data de
|
||||
28.02.2017
|
||||
soft J*
|
||||
actualizat în data de
|
||||
16.02.2017
|
||||
Anexa
|
||||
validări
|
||||
publicat în data de
|
||||
19.01.2017
|
||||
Schema
|
||||
XSD
|
||||
publicat în data de
|
||||
19.01.2017
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile realizate, pe beneficiari de venit, conform Ordinului ANAF 3605/ 2015 ( începând cu anul de raportare 2015 ) - publicat în
|
||||
11.01.2016
|
||||
soft A
|
||||
actualizat în data de
|
||||
18.01.2016
|
||||
soft J*
|
||||
actualizat în data de
|
||||
12.09.2016
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
04.02.2016
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile realizate, pe beneficiari de venit, conform Ordinului ANAF 3883/2013 ( începând cu anul de raportare 2013 ) - actualizat în
|
||||
20.02.2015
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
205
|
||||
- Declaraţie informativă privind impozitul reţinut la sursă şi câştigurile/pierderile realizate, pe beneficiari de venit, conform Ordinului ANAF 1913/2012 -( valabil pentru an raportare 2012 ) actualizat în
|
||||
09.04.2013
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
180
tools/anaf-monitor/snapshots/D300.txt
Normal file
180
tools/anaf-monitor/snapshots/D300.txt
Normal file
@@ -0,0 +1,180 @@
|
||||
|
||||
300
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 2131/02.09.2025, utilizat începând cu declararea obligaţiilor fiscale aferente lunii ianuarie 2026 - publicat în data
|
||||
11.02.2026
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 2131/02.09.2025, utilizat începând cu declararea obligaţiilor fiscale aferente lunii august 2025 - publicat în data
|
||||
03.09.2025
|
||||
soft A
|
||||
actualizat în data de
|
||||
16.12.2025
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
publicat în data de
|
||||
23.09.2025
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 888/29.04.2024, utilizat începând cu declararea obligaţiilor fiscale aferente lunii ianuarie 2025 (Contine cod CAEN Rev 3. Pentru cod CAEN Rev 2 va rugam sa utilizati versiunea anterioara)- publicat în data
|
||||
12.02.2025
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 888/29.04.2024, utilizat începând cu declararea obligaţiilor fiscale aferente lunii mai 2024 - publicat în data
|
||||
30.05.2024
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
05.06.2024
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 1176/04.08.2023, utilizat începând cu declararea obligaţiilor fiscale aferente lunii august 2023 - publicat în data
|
||||
28.08.2023
|
||||
soft A
|
||||
actualizat în data de
|
||||
22.09.2023
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
28.08.2023
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
25.09.2023
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 1253/2021, utilizat începând cu declararea obligaţiilor fiscale aferente lunii iulie 2021 valabil de la data 17.08.2021 - publicat în data
|
||||
18.08.2021
|
||||
soft A
|
||||
actualizat în data de
|
||||
28.03.2022
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
08.07.2021
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
12.07.2021
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 632/2021, utilizat începând cu declararea obligaţiilor fiscale aferente lunii iunie 2021 valabil de la data 01.07.2021 - publicat în data
|
||||
01.07.2021
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
08.07.2021
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
12.07.2021
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 2227/2019, utilizat începând cu declararea obligaţiilor fiscale valabil de la data 01.07.2019 - publicat în data
|
||||
13.08.2019
|
||||
soft A
|
||||
actualizat în data de
|
||||
02.09.2019
|
||||
soft J*
|
||||
actualizat în data de
|
||||
18.09.2019
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
14.08.2019
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
19.08.2019
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform
|
||||
OPANAF nr. 591/2017, utilizat începând cu declararea obligaţiilor fiscale valabil de la data 01.01.2017 - publicat în data
|
||||
31.01.2017
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform OPANAF nr. 588/2016, utilizat începând cu declararea obligaţiilor fiscale valabil de la data 01.01.2016 - publicat în data
|
||||
15.02.2016
|
||||
soft A
|
||||
23.03.2016
|
||||
soft J*
|
||||
24.02.2016
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform OPANAF
|
||||
nr. 1790/2012, utilizat începând cu declararea obligaţiilor fiscale
|
||||
valabil de la data 01.01.2013
|
||||
-
|
||||
actualizat în data de
|
||||
24.01.2014
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D300
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform OPANAF nr.
|
||||
3665/22.12.2011, utilizat începând cu declararea obligaţiilor fiscale
|
||||
valabil de la data 01.01.2012
|
||||
-
|
||||
actualizat în data de
|
||||
06.03.2012
|
||||
soft A
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D300
|
||||
300
|
||||
- Decont de taxă pe valoarea adăugată conform OPANAF nr. 183/31.01.2011, utilizat începând cu declararea obligaţiilor fiscale aferente lunii noiembrie 2011
|
||||
-
|
||||
actualizat în data de
|
||||
15.01.2012
|
||||
soft A
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D300
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
68
tools/anaf-monitor/snapshots/D390.txt
Normal file
68
tools/anaf-monitor/snapshots/D390.txt
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
390
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
390
|
||||
- Declaraţia recapitulativă privind livrările/achiziţiile/prestările intracomunitare
|
||||
-
|
||||
conform OPANAF nr. 705/11.03.2020 - incepand cu perioada de raportare
|
||||
02/2020
|
||||
-
|
||||
publicat în data de
|
||||
18.03.2020
|
||||
soft A
|
||||
actualizat în data de
|
||||
07.05.2024
|
||||
soft J*
|
||||
actualizat în data de
|
||||
25.06.2025
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
07.05.2024
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
12.02.2021
|
||||
390
|
||||
- Declaraţia recapitulativă privind livrările/achiziţiile/prestările intracomunitare
|
||||
-
|
||||
incepand cu anul de raportare 2017
|
||||
-
|
||||
publicat în data de
|
||||
01.02.2017
|
||||
soft A
|
||||
actualizat în data de
|
||||
30.01.2020
|
||||
soft J*
|
||||
actualizat în data de
|
||||
12.02.2020
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
390
|
||||
- Declaraţia recapitulativă privind livrările/achiziţiile/prestările intracomunitare
|
||||
-conform OPANAF nr.591/03.02 2016 (publicat în M.O. nr.94/ 08.02.2016)
|
||||
-începand cu anul de raportare 2015
|
||||
-conform OPANAF nr.76/21.01.2010 - incepand cu anul de raportare 2011
|
||||
-
|
||||
actualizat în data de
|
||||
09.02.2016
|
||||
soft A
|
||||
actualizat în data de
|
||||
09.02.2016
|
||||
soft J*
|
||||
actualizat în data de
|
||||
23.04.2015
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D390
|
||||
Prevederi legale de completare a formularului 390
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
141
tools/anaf-monitor/snapshots/D394.txt
Normal file
141
tools/anaf-monitor/snapshots/D394.txt
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
394
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
394
|
||||
Declaraţie informativă privind livrările/prestările şi achiziţiile efectuate pe teritoriul naţional - utilizat începând cu declararea obligaţiilor fiscale aferente lunii august 2025 - publicat în
|
||||
15.09.2025
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
394
|
||||
Declaraţie informativă privind livrările/prestările şi achiziţiile efectuate pe teritoriul naţional conform OPANAF 77/2022- utilizat începând cu declararea obligaţiilor fiscale aferente lunii ianuarie 2025 (Contine cod CAEN Rev 3. Pentru cod CAEN Rev 2 va rugam sa utilizati versiunea anterioara) - publicat în
|
||||
12.02.2025
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
394
|
||||
Declaraţie informativă privind livrările/prestările şi achiziţiile efectuate pe teritoriul naţional conform OPANAF 77/2022- utilizat începând cu declararea obligaţiilor fiscale aferente lunii aprilie 2022 - publicat în
|
||||
10.05.2022
|
||||
soft A
|
||||
soft J*
|
||||
actualizat în data de
|
||||
30.05.2022
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
394
|
||||
Declaraţie informativă privind livrările/prestările şi achiziţiile efectuate pe teritoriul naţional conform OPANAF 3281/2020- utilizat începând cu declararea obligaţiilor fiscale aferente lunii septembrie 2020 - publicat în
|
||||
02.09.2020
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
25.09.2020
|
||||
394
|
||||
Declaraţie informativă privind
|
||||
livrările/prestările şi achiziţiile efectuate pe teritoriul naţional
|
||||
conform OPANAF 2264/2016 - utilizat începând cu declararea
|
||||
obligaţiilor fiscale aferente lunii ianuarie 2017 - publicat în
|
||||
31.01.2017
|
||||
soft A
|
||||
actualizat în data de
|
||||
13.02.2018
|
||||
soft J*
|
||||
actualizat în data de
|
||||
09.09.2019
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
07.09.2017
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
09.09.2019
|
||||
394
|
||||
Declaraţie informativă privind
|
||||
livrările/prestările şi achiziţiile efectuate pe teritoriul naţional
|
||||
conform OPANAF 2264/2016 - utilizat începând cu declararea
|
||||
obligaţiilor fiscale aferente lunii iulie 2016 - publicat în
|
||||
02.08.2016
|
||||
soft A
|
||||
actualizat în data de
|
||||
08.12.2016
|
||||
soft J*
|
||||
actualizat în data de
|
||||
19.01.2017
|
||||
Anexa
|
||||
validări
|
||||
actualizat în data de
|
||||
07.12.2016
|
||||
Schema
|
||||
XSD
|
||||
actualizat în data de
|
||||
18.11.2016
|
||||
394
|
||||
Declaraţie informativă privind livrările/prestarile
|
||||
şi achiziţiile efectuate pe teritoriul naţional conform
|
||||
OPANAF 3806/2013 - utilizat începând cu declararea obligaţiilor fiscale aferente lunii
|
||||
ianuarie 2016 - actualizat în
|
||||
06.04.2016
|
||||
soft A
|
||||
actualizat în data de
|
||||
06.04.2016
|
||||
soft J*
|
||||
actualizat în data de
|
||||
06.04.2016
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
-
|
||||
actualizat în data de
|
||||
06.04.2016
|
||||
Ghid
|
||||
de depunere a declaraţiei D394
|
||||
Prevederi legale de completare a formularului 394
|
||||
394
|
||||
Declaraţie informativă privind livrările/prestarile
|
||||
şi achiziţiile efectuate pe teritoriul naţional conform
|
||||
OPANAF 3806/2013 - utilizat începând cu declararea obligaţiilor fiscale aferente lunii
|
||||
decembrie 2013 - actualizat în
|
||||
19.03.2015
|
||||
soft A
|
||||
actualizat în data de
|
||||
29.01.2016
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
-
|
||||
actualizat în data de
|
||||
06.04.2016
|
||||
Ghid
|
||||
de depunere a declaraţiei D394
|
||||
Prevederi legale de completare a formularului 394
|
||||
394
|
||||
Declaraţie informativă privind livrările/prestarile şi achiziţiile efectuate pe teritoriul naţional conform OPANAF 3596/2011 publicat în MO 927/28.12.2011 - utilizat începând cu declararea obligaţiilor fiscale aferente lunii ianuarie 2012 - actualizat în
|
||||
28.10.2013
|
||||
soft A
|
||||
soft J*
|
||||
Anexa
|
||||
validări
|
||||
Schema
|
||||
XSD
|
||||
Ghid
|
||||
de depunere a declaraţiei D394
|
||||
Prevederi legale de completare a formularului 394
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
21
tools/anaf-monitor/snapshots/D406.txt
Normal file
21
tools/anaf-monitor/snapshots/D406.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
ANAF
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
D406
|
||||
- Declaraţia SAF-T conform OPANAF 1783 din 04.11.2021 privind natura informaţiilor pe care contribuabilul/platitorul trebuie să le declare prin fişierul standard de control fiscal,
|
||||
modelul de raportare, procedura şi condiţiile de transmitere, precum şi termenele de transmitere şi data/datele de la care categoriile de contribuabili/plătitori sunt obligate să transmită fişierul standard de control fiscal - publicat în
|
||||
28.02.2022
|
||||
Soft J*
|
||||
actualizat în data de
|
||||
11.02.2026
|
||||
Schema xsd
|
||||
actualizat în data de
|
||||
08.07.2025
|
||||
Schema xlsx
|
||||
actualizat în data de
|
||||
11.02.2026
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
566
tools/anaf-monitor/snapshots/DESCARCARE_DECLARATII.txt
Normal file
566
tools/anaf-monitor/snapshots/DESCARCARE_DECLARATII.txt
Normal file
@@ -0,0 +1,566 @@
|
||||
Descarcare declaratii
|
||||
Ca urmare a ultimei
|
||||
actualizări realizate de catre ADOBE, există posibilitatea să apară erori în
|
||||
procesul de semnare electronică a declaraţiilor fiscale. în aceste situaţii,
|
||||
pentru remediere trebuie să urmaţi procedurile indicate de furnizorul
|
||||
certificatului dumneavoastră.
|
||||
Dacă aveţi probleme cu descărcarea formularelor, vă rugăm să folosiţi
|
||||
Formularul de contact
|
||||
alegând categoria "Asistenţă tehnică servicii
|
||||
informatice".
|
||||
Data ultimei actualizări: 19.05.2023
|
||||
Pentru a putea folosi softul J trebuie să aveţi instalat programul
|
||||
DUKIntegrator
|
||||
- actualizat în data de 20.08.2021
|
||||
Dezvoltatorii de sisteme informatice pot descărca
|
||||
sursele
|
||||
aplicaţiei DUKIntegrator - actualizat în data de 11.09.2018
|
||||
Document de confirmare
|
||||
- actualizat în data de 29.06.2021
|
||||
Cu scopul de a mări gradul de rezilienţă şi de a asigura securitatea resurselor şi a sistemelor informatice, la nivel global au fost actualizate procedurile
|
||||
şi politicile de securitate. În acest scop, pentru a nu afecta procesul de digitalizare a Ministerului Finanţelor (ANAF) şi pentru a urma standardele înalte de securitate stabilite
|
||||
la nivel mondial, vă informăm că începând cu 08.01.2023 aplicaţiile puse la dispoziţia contribuabililor (pdf-urile inteligente) vor putea fi utilizate numai pe sisteme de operare
|
||||
aflate în suport la producător şi cu toate update-urile la zi.
|
||||
D11
|
||||
- CERERE DE ANULARE a unor obligații bugetare/ NOTIFICARE privind intenția de a beneficia de anularea unor obligații bugetare
|
||||
|
||||
- publicat în data de
|
||||
20.09.2024
|
||||
Se transmit prin portalul SPV
|
||||
CA300
|
||||
Cerere de acord pentru finanţare, în baza H.G. nr. 300/2024 - publicat în data de
|
||||
16.01.2026
|
||||
C300
|
||||
Formular ataşare documente, în baza H.G. nr. 300/2024 - publicat în data de
|
||||
18.12.2025
|
||||
CP300
|
||||
Cerere de plată, în baza H.G. nr. 300/2024 - publicat în data de
|
||||
16.12.2025
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
CA807
|
||||
Cerere de acord pentru finanţare, în baza H.G. nr. 807/2014 - publicat în data de
|
||||
12.04.2021
|
||||
C807
|
||||
Formular ataşare documente, în baza H.G. nr. 807/2014 - publicat în data de
|
||||
12.04.2021
|
||||
CP807
|
||||
Cerere de plată a ajutorului de stat, în baza H.G. nr.807/2014 - publicat în data de
|
||||
12.04.2021
|
||||
Se transmit prin SPV
|
||||
F1129
|
||||
- Ordinul de plată multiplu electronic (OPME) V.2.0.45 dată
|
||||
actualizare
|
||||
25.11.2025
|
||||
Formularul se depune on-line prin Sistemul naţional de raportare
|
||||
FOREXEBUG
|
||||
de către instituţiile publice şi, respectiv, prin portalul
|
||||
e-guvernare.ro
|
||||
de către operatorii economici şi alte entităţi decât instituţii
|
||||
publice
|
||||
173
|
||||
- Cerere privind grupul fiscal în domeniul impozitului pe profit. -
|
||||
publicat în data de
|
||||
29.10.2021
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
D708
|
||||
- INFORMARE privind alegerea
|
||||
efectuării în România a procedurilor de raportare, conform
|
||||
prevederilor secţiunii IV din anexa 5 la legea nr.207/2015 privind
|
||||
Codul de Procedură Fiscală, pentru operatorii de platformă care îndeplinesc condiţiile
|
||||
menţionate la pct.4, lit. a, subsecţiunea A, secţiunea I din anexa V la legea nr. 207/2015 privind Codul de Procedură Fiscală
|
||||
şi în România şi în alte state membre. -
|
||||
publicat în data de
|
||||
28.12.2023
|
||||
Formulare utilizate de operatorii de platforme digitale
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Z01
|
||||
- Cerere pentru eliberarea certificatului de rezidenţă fiscală
|
||||
pentru persoane juridice rezidente în România / Application for the issuance of the certificate of tax residence for resident legal persons in Romania - publicat în data de
|
||||
26.04.2021
|
||||
Z03
|
||||
- Cerere pentru eliberarea certificatului de rezidenţă fiscală pentru persoane fizice rezidente în România /
|
||||
Application for the issuance of the certificate of tax residence for individuals resident in Romania - publicat în data de
|
||||
26.04.2021
|
||||
Z05
|
||||
- Cerere pentru eliberarea certificatului de rezidenţă fiscală pentru persoane fizice rezidente în România care desfăşoară activitate independentă /
|
||||
Application for the issuance of the certificate of tax residence for individuals resident in Romania carrying on an independent activity - publicat în data de
|
||||
26.04.2021
|
||||
Z07
|
||||
- Cerere pentru eliberarea certificatului de rezidenţă fiscală pentru persoane rezidente în România / Application for the issuance of the certificate of tax residence for persons resident in Romania - publicat în data de
|
||||
26.04.2021
|
||||
Z09
|
||||
- Cerere pentru eliberarea certificatului privind atestarea impozitului plătit în România de persoane juridice
|
||||
străine / Application for the issuance of the certificate attesting the tax paid in Romania by foreign legal persons - publicat în data de
|
||||
26.04.2021
|
||||
Z11
|
||||
- Cerere pentru eliberarea certificatului privind atestarea impozitului plătit în România de persoane fizice nerezidente / Application for the issuance of the certificate attesting the tax paid in Romania by nonresident individuals - publicat în data de
|
||||
26.04.2021
|
||||
Z13
|
||||
- Cerere referitoare la eliberarea
|
||||
certificatului privind atestarea desfăşurării activităţii în România de
|
||||
către sediul permanent al unei persoane juridice străine / Application for the issuance of the certificate attesting the activity carried on in Romania by the permanent establishment/designated permanent establishment of a foreign legal person - publicat în data de
|
||||
26.04.2021
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
şi prin link-ul
|
||||
Depunere Declaraţie unică şi alte formulare SPV-PF
|
||||
It can be sent through
|
||||
e-guvernare.ro
|
||||
and link
|
||||
"
|
||||
Depunere Declaraţie unică şi alte formulare SPV-PF
|
||||
"
|
||||
Z015
|
||||
- Chestionar pentru stabilirea rezidenţei fiscale a
|
||||
persoanei fizice la sosirea în România
|
||||
Set of questions for determining the fiscal residence of the individual on the arrival in Romania - publicat în data de
|
||||
07.04.2020
|
||||
Z017
|
||||
- Chestionar pentru stabilirea rezidenţei fiscale a
|
||||
persoanei fizice la plecarea din România
|
||||
Set of questions for determining the
|
||||
fiscal residence of the individual when leaving Romania - publicat
|
||||
în data de
|
||||
07.04.2020
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
şi prin link-ul
|
||||
Depunere Declaraţie unică şi alte formulare SPV-PF
|
||||
It can be sent through
|
||||
e-guvernare.ro
|
||||
and link
|
||||
"
|
||||
Depunere Declaraţie unică şi alte formulare SPV-PF
|
||||
"
|
||||
163
|
||||
- Cerere de înscriere /radiere în /din Registrul entităţilor
|
||||
/unităţilor de cult pentru care se acorda deduceri fiscale -
|
||||
publicat în data de
|
||||
01.04.2019
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
168
|
||||
-
|
||||
Cerere de înregistrare a contractelor de locaţiune - actualizat în data de
|
||||
04.01.2023
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
şi prin link-ul
|
||||
Depunere Declaraţie unică şi alte formulare SPV-PF
|
||||
169
|
||||
-
|
||||
DECLARAŢIE DE ÎNREGISTRARE A CONTRACTELOR DE FIDUCIE (anexa 3) conform cu prevederile OPANAF 1193/ 2021 - publicat în data de
|
||||
18.04.2022
|
||||
169N
|
||||
-
|
||||
Declaraţie privind neconcordanţele între informaţiile privind beneficiarii reali, disponibile în Registrul central al fiduciilor şi al construcţiilor
|
||||
juridice similare fiduciilor şi informaţiile deţinute de autorităţi/ entităţi raportoare (anexa 6) în conformitate cu OPANAF nr.1193/ 2021 - publicat în data de
|
||||
18.04.2022
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Declaraţii electronice
|
||||
(098)
|
||||
Se
|
||||
depun la organul fiscal competent. Nu se transmit on-line.
|
||||
Declaraţie privind nedeductibilitatea TVA aferente cheltuielilor
|
||||
efectuate în cadrul operaţiunii finanţate din FEDR, FSE şi FC 2014 -
|
||||
2020
|
||||
- publicat în data de
|
||||
28.09.2016
|
||||
Se depun la organul
|
||||
fiscal competent. Nu se transmit on-line.
|
||||
Declaraţii
|
||||
electronice
|
||||
(
|
||||
017
|
||||
,
|
||||
060
|
||||
,
|
||||
093
|
||||
,
|
||||
100
|
||||
,
|
||||
101
|
||||
,
|
||||
101G
|
||||
,
|
||||
104
|
||||
,
|
||||
106
|
||||
,
|
||||
107
|
||||
,
|
||||
108
|
||||
,
|
||||
109
|
||||
,
|
||||
110
|
||||
,
|
||||
112
|
||||
,
|
||||
114
|
||||
,
|
||||
119
|
||||
,
|
||||
120
|
||||
,
|
||||
130
|
||||
,
|
||||
177
|
||||
,
|
||||
179
|
||||
,
|
||||
180
|
||||
,
|
||||
182
|
||||
,
|
||||
205
|
||||
,
|
||||
207
|
||||
,
|
||||
213
|
||||
,
|
||||
214
|
||||
,
|
||||
216
|
||||
,
|
||||
217
|
||||
,
|
||||
222
|
||||
,
|
||||
300
|
||||
,
|
||||
301
|
||||
,
|
||||
307
|
||||
,
|
||||
311
|
||||
,
|
||||
390
|
||||
,
|
||||
392
|
||||
,
|
||||
393
|
||||
,
|
||||
394
|
||||
,
|
||||
395
|
||||
,
|
||||
397
|
||||
,
|
||||
398
|
||||
,
|
||||
401
|
||||
,
|
||||
402
|
||||
,
|
||||
403
|
||||
,
|
||||
404
|
||||
,
|
||||
405
|
||||
,
|
||||
406
|
||||
,
|
||||
407
|
||||
,
|
||||
408
|
||||
,
|
||||
700
|
||||
,
|
||||
710,
|
||||
711,
|
||||
B900,
|
||||
B901,
|
||||
B902
|
||||
|
||||
|
||||
- actualizat în data de
|
||||
28.10.2024
|
||||
(C182)
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Declarațiile 216 şi 217 se pot transmite și prin SPV de către persoanele fizice.
|
||||
Cererea C182 se poate transmite si prin SPV.
|
||||
Declaraţie 089
|
||||
Declaraţie pe propria răspundere pentru îndeplinirea condiţiei prevăzute la art. 331 alin. (2) lit. e) pct. 2 şi/sau art. 331 alin. (2) lit. l) pct. 2 din Codul fiscal
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Formular C310
|
||||
Cerere pentru aprobarea transferului sumelor din contul de TVA
|
||||
Declaraţie
|
||||
pe propria răspundere pentru înregistrarea în scopuri
|
||||
de TVA, potrivit art. 316 alin. (1) lit. c) din Legea nr. 227/2015
|
||||
privind Codul Fiscal, cu modificările şi completările ulterioare -
|
||||
publicat în data de
|
||||
01.02.2019
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Declaraţie D318
|
||||
Cerere de rambursare a TVA pentru persoanele impozabile stabilite în
|
||||
România, depusă potrivit art. 302 alin.(2) din Codul fiscal -
|
||||
publicat în
|
||||
19.10.2017
|
||||
Declaraţie 319
|
||||
Declaraţie de ajustare a pro-ratei - publicat în
|
||||
04.12.2017
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Formular S1055
|
||||
înştiinţare privind modificarea exerciţiului
|
||||
financiar în baza art. 27 din legea contabilităţii
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Notificare
|
||||
privind modificarea anului fiscal -
|
||||
publicat în data de
|
||||
02.04.2020
|
||||
Notificare
|
||||
privind modificarea sistemului
|
||||
anual/trimestrial de declarare şi plată a impozitului pe profit -
|
||||
publicat în data de
|
||||
13.04.2020
|
||||
Se
|
||||
transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Notificare/Cerere de anulare a obligaţiilor de plată accesorii
|
||||
(D2
|
||||
,
|
||||
D3)
|
||||
-
|
||||
actualizat în data de
|
||||
07.08.2020
|
||||
Se transmit prin SPV
|
||||
Cerere de acordare a eşalonării la plată
|
||||
(D5)
|
||||
- publicat în data de
|
||||
19.11.2020
|
||||
Se transmit prin SPV
|
||||
Cerere de restructurare a obligaţiilor bugetare/Notificare privind
|
||||
intenţia de restructurare a obligaţiilor bugetare
|
||||
(D6)
|
||||
- publicat în data de
|
||||
14.12.2020
|
||||
Se transmit prin SPV
|
||||
Cerere de acordare a eşalonării la plată, în forma simplificată
|
||||
(D7)
|
||||
- publicat în data de
|
||||
29.12.2021
|
||||
Se transmit prin SPV
|
||||
Situaţia eliberărilor pentru consum de ţigarete, ţigări şi ţigări de foi, tutun
|
||||
fin tăiat destinat rulării în ţigarete şi alte tutunuri de fumat
|
||||
(D9)
|
||||
- publicat în data de
|
||||
11.01.2023
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Declaraţie privind preţurile de vânzare cu amănuntul
|
||||
pe sortimente de ţigarete
|
||||
(D10)
|
||||
- publicat în data de
|
||||
11.01.2023
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Cerere de acordare a eşalonării la plată
|
||||
(D8)
|
||||
- publicat în data de
|
||||
12.07.2022
|
||||
Se transmit prin SPV
|
||||
Situaţii financiare interimare trimestriale
|
||||
<
|
||||
=2024
|
||||
- actualizat în data de
|
||||
20.04.2023
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Situaţii financiare interimare trimestriale =2025
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Raportări contabile semestriale 2024
|
||||
- publicat în data de
|
||||
17.07.2024
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Raportări contabile semestriale 2025
|
||||
- publicat în data de
|
||||
18.07.2025
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Situaţii financiare anuale/Raportări anuale an 2022
|
||||
- publicat în data de
|
||||
20.01.2023
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Situaţii financiare anuale/Raportări anuale an 2023
|
||||
- publicat în data de
|
||||
17.11.2023
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Situaţii financiare anuale/Raportări anuale an 2024
|
||||
- publicat în data de
|
||||
28.01.2025
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Situaţii financiare anuale/Raportări anuale an 2025
|
||||
- publicat în data de
|
||||
10.02.2025
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Arhivă situaţii financiare anuale/Raportări anuale
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Arhivă raportări contabile semestriale
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Formulare S1001
|
||||
,
|
||||
1100
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Declaraţii electronice
|
||||
(
|
||||
Declaraţia unică (212)
|
||||
,
|
||||
200
|
||||
,
|
||||
201
|
||||
,
|
||||
204
|
||||
,
|
||||
208
|
||||
,
|
||||
209
|
||||
,
|
||||
220
|
||||
,
|
||||
221
|
||||
,
|
||||
223
|
||||
,
|
||||
224
|
||||
,
|
||||
230
|
||||
,
|
||||
600
|
||||
603
|
||||
)
|
||||
|
||||
- actualizat în data de
|
||||
28.04.2022
|
||||
(D603)
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Declaraţie unică se depune accesând link-ul:
|
||||
Depunere Declaraţie unică şi alte formulare SPV-PF
|
||||
D200 se poate depune prin SPV
|
||||
DAC6
|
||||
- Formular utilizat de intermediarii sau contribuabilii relevanţi, după caz, în vederea raportării informaţiilor cu privire la aranjamentele transfrontaliere care fac obiectul raportării
|
||||
- publicat în data de
|
||||
20.01.2021
|
||||
Se transmit prin
|
||||
portalul
|
||||
e-guvernare.ro
|
||||
Formulare pentru sistemul PATRIMVEN:
|
||||
P1000
|
||||
- actualizat în data de
|
||||
28.02.2022
|
||||
,
|
||||
P2000
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
sau prin sistemul PATRIMVEN:
|
||||
epatrim.fiscnet.ro
|
||||
sau
|
||||
epatrim.anaf.ro
|
||||
Formularul
|
||||
F3000
|
||||
- publicat în data de
|
||||
24.08.2016
|
||||
Formularul
|
||||
F7000
|
||||
- publicat în data de
|
||||
12.01.2024
|
||||
Se transmit prin portalul
|
||||
e-guvernare.ro
|
||||
Formularul
|
||||
P4000
|
||||
,
|
||||
P5000
|
||||
Formularul
|
||||
L153
|
||||
Se transmit prin
|
||||
portalul
|
||||
e-guvernare.ro
|
||||
C801
|
||||
- Cerere de atribuire a numărului unic de identificare din aplicaţia
|
||||
informatică a ANAF - publicat în data de
|
||||
16.07.2018
|
||||
C802
|
||||
- Cerere pentru obţinerea profilurilor de trecere a aparatelor de marcat electronice fiscale în funcţionare online - publicat în data de
|
||||
30.03.2021
|
||||
Se transmit prin SPV
|
||||
F4101
|
||||
- Formulare utilizate pentru declararea în etapa
|
||||
pre-operaţionalizare a Registrului Aparatelor de Marcat Electronice
|
||||
Fiscale - publicat în data de
|
||||
29.05.2018
|
||||
F4102
|
||||
- Formular pentru înregistrarea aparatelor de marcat electronice
|
||||
fiscale (AMEF) instalate - publicat în data de
|
||||
11.06.2018
|
||||
F4103
|
||||
- Formular pentru înregistrarea unor schimbări privind starea/
|
||||
situaţia precum şi livrarea/achiziţia aparatelor de marcat
|
||||
electronice fiscale - publicat în data de
|
||||
31.07.2018
|
||||
F4105
|
||||
- Notificare privind situaţia aparatului de marcat electronic fiscal
|
||||
(Anexa 2E la Metodologia şi procedura de înregistrare a aparatelor
|
||||
de marcat electronice fiscale în etapa pre-operaţionalizare a
|
||||
Registrului), în conformitate cu OPANAF nr.4156/ 28.12.2017 -
|
||||
publicat în data de
|
||||
07.08.2018
|
||||
F4109
|
||||
- Declaraţie privind aparatele de marcat electronice fiscale (AMEF)
|
||||
neutilizate - publicat în data de
|
||||
17.09.2018
|
||||
F4110
|
||||
- Declaraţie pe propria răspundere prin care utilizatorul declară că utilizează aparatul de marcat electronic fiscal instalat în zone nedeservite de reţele de comunicaţii electronice, în conformitate cu OPANAF nr. 435/2021 - publicat în data de
|
||||
30.03.2021
|
||||
A4200
|
||||
- aplicaţie pentru validarea fişierului XML creat de aparatele de
|
||||
marcat electronice fiscale, conţinând registrul rapoartelor Z şi
|
||||
bonurilor fiscale pentru perioada de raportare curentă, prevăzut în
|
||||
secţiunea II.12 din anexa nr. 2 la OPANAF nr. 146/2018 - publicat
|
||||
în data de
|
||||
10.09.2018
|
||||
A4201
|
||||
- aplicaţie pentru validarea fişierului XML creat de aparatele de
|
||||
marcat electronice fiscale, în activitatea de schimb valutar,
|
||||
conţinând bonurile fiscale (sectiunea II.1) şi raportul Z (secţiunea
|
||||
II.5) pentru o zi fiscală, în conformitate cu anexa 2 la OPANAF nr.
|
||||
146/2018 - publicat în data de
|
||||
14.01.2019
|
||||
A4202
|
||||
- aplicaţie pentru validarea fişierului XML creat de aparatele de
|
||||
marcat electronice fiscale, utilizate în activitatea de taximetrie
|
||||
conţinând registrul rapoartelor Z şi bonurilor fiscale pentru
|
||||
perioada de raportare curentă, prevazut în secţiunea II.2 din anexa
|
||||
nr. 2 la OPANAF nr. 146/2018 - publicat în data de
|
||||
14.01.2019
|
||||
A4203
|
||||
- aplicaţie pentru validarea fişierelor XML create de aparatele de
|
||||
marcat electronice fiscale altele decât cele utilizate în
|
||||
activitatea de schimb valutar sau în activitatea de taximetrie
|
||||
(inclusiv cele utilizate în aeroporturi), conţinând bonurile fiscale
|
||||
(secţiunea II.3) şi raportul Z (secţiunea II.7) pentru o zi fiscală,
|
||||
în conformitate cu anexa nr. 2 la OPANAF nr. 146/2018 - publicat în
|
||||
data de
|
||||
10.09.2018
|
||||
Se transmit prin
|
||||
portalul
|
||||
e-guvernare.ro
|
||||
^
|
||||
33
tools/anaf-monitor/snapshots/SIT_FIN_AN_2025.txt
Normal file
33
tools/anaf-monitor/snapshots/SIT_FIN_AN_2025.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
S1030
|
||||
S1002-S1003-S1004-S1005
|
||||
S1011
|
||||
S1019
|
||||
S1025
|
||||
S1030
|
||||
S1039
|
||||
S1040-S1041
|
||||
S1042
|
||||
S1046
|
||||
S1051-S1052-S1053-S1054
|
||||
Tabel
|
||||
codificări
|
||||
tipuri de situaţii financiare şi raportări anuale
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
S1030
|
||||
- Situaţii financiare anuale consolidate La art. 185 alin. (1) - (2) din Legea societăţilor nr. 31/1990 republicată, modificată şi completată prin Ordonanţa de urgenţă a Guvernului nr. 90/2010, se prevăd următoarele: "(1) în condiţiile prevăzute de Legea contabilităţii nr. 82/1991, republicată, consiliul de administraţie, respectiv directoratul, este obligat să depună la unităţile teritoriale ale Ministerului Finanţelor, în formă electronică, având atasată o semnătură electronică extinsă, situaţiile financiare anuale, raportul lor, raportul cenzorilor sau raportul auditorilor financiari, după caz. (2) Consiliul de administraţie, respectiv directoratul societătii-mamă, definită astfel de reglementările contabile aplicabile, este obligat să depună la unităţile teritoriale ale Ministerului Finanţelor copii ale situaţiilor financiare anuale consolidate, prevederile alin. (1) urmând a fi aplicate în mod corespunzător. Totodată, potrivit art. 29 alin. (2), art. 33 alin. (2) şi art. 36 alin. (5) din Legea contabilităţii nr. 82/1991, republicată, cu modificările şi completările ulterioare, o societate-mamă trebuie să întocmească atât situaţii financiare anuale pentru propria activitate, cât şi situaţii financiare anuale consolidate, în condiţiile prevăzute de reglementările contabile aplicabile. Situaţiile financiare anuale consolidate constituie un tot unitar şi se întocmesc în termen de 8 luni de la încheierea exerciţiului financiar al societăţii-mamă. Acestea cuprind bilanţul consolidat, contul de profit şi pierdere consolidat, precum şi celelalte componente, respectiv informaţii referitoare la activitatea grupului, potrivit reglementărilor contabile aplicabile, şi note explicative la situaţiile financiare anuale consolidate. Conform art. 185 alin. (2) şi alin. (3) din Legea societăţilor nr. 31/1990, modificată şi completată prin Ordonanţa de urgenţă a Guvernului nr. 90/2010 şi art. 31 din Legea contabilităţii nr. 82/1991, republicată, cu modificările şi completările ulterioare, situaţiile financiare anuale consolidate depuse la unităţile teritoriale ale Ministerului Finanţelor sunt însoţite de raportul consolidat al administratorilor, raportul de audit şi de o declaraţie scrisă a persoanelor prevăzute de legea contabilităţii. Consiliul de administraţie, respectiv directoratul societăţii-mamă, definită astfel de reglementările contabile aplicabile, este obligat ca în termen de 15 zile de la data aprobării acestora să depună la unităţile teritoriale ale Ministerului Finanţelor situaţiile financiare anuale consolidate, potrivit prevederilor legale în vigoare.
|
||||
Conform reglementărilor contabile aplicabile, entităţile care au obligaţia să întocmească situaţii financiare anuale consolidate pot întocmi aceste situaţii fie potrivit Reglementărilor contabile privind situaţiile financiare anuale individuale şi situaţiile financiare anuale consolidate, aprobate prin Ordinul ministrului finanţelor publice nr. 1802/2014, cu modificările şi completările ulterioare, fie în baza Standardelor Internaţionale de Raportare Financiară (IFRS), după caz. În vederea depunerii situaţiilor financiare anuale consolidate în formă electronică, având atasată o semnătură electronică, se foloseste programul de asistentă pus la dispozitie gratuit de către Ministerul Finanţelor pe site-ul ANAF, prin care se generează un fisier de tip PDF, având atasat un fisier xml, care conţine datele de identificare a societăţii-mamă şi ale entităţilor care sunt cuprinse în situaţiile financiare anuale consolidate precum şi un fisier cu extensia .zip. Fisierul cu extensia .zip va conţine situaţiile financiare anuale consolidate şi documentele cerute de lege scanate alb-negru, lizibil şi cu o rezoluţie care să permită încadrarea în limita a 9,5 MB a fisierului PDF la care este atasat fisierul zip. Fişierul zip ataşat situaţiilor financiare anuale nu va conţine parolă - publicat în data de
|
||||
05.02.2025
|
||||
soft A
|
||||
actualizat în data
|
||||
27.01.2026
|
||||
S1030 -Soft J
|
||||
Schema XSD
|
||||
:
|
||||
S1030
|
||||
Structura
|
||||
S1030
|
||||
*softul J se adresează doar contribuabililor care îşi generează fişierul xml din aplicaţiile informatice proprii
|
||||
37
tools/anaf-monitor/snapshots/SIT_FIN_SEM_2025.txt
Normal file
37
tools/anaf-monitor/snapshots/SIT_FIN_SEM_2025.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
S1012
|
||||
S1012
|
||||
S1013
|
||||
S1027-S1050
|
||||
S1029
|
||||
S1031-S1032-S1033
|
||||
S1034
|
||||
S1035-S1078
|
||||
S1036-S1038
|
||||
S1045
|
||||
S1057
|
||||
S1058
|
||||
S1059-S1060
|
||||
S1074
|
||||
Tabel codificări
|
||||
tipuri de situaţii financiare şi raportări anuale
|
||||
Denumire formular
|
||||
Programe asistenţă
|
||||
Instrucţiuni/ Documentaţie
|
||||
PDF
|
||||
JAVA
|
||||
S1012
|
||||
- Sistemul de raportare contabilă la
|
||||
30 iunie 2025
|
||||
pentru
|
||||
societăţile din domeniul asigurărilor
|
||||
(asiguratori) - publicat în data de
|
||||
18.07.2025
|
||||
soft A
|
||||
Soft J - 1012
|
||||
Schema XSD
|
||||
: S1012
|
||||
Structura
|
||||
S1012
|
||||
*softul J se adresează doar contribuabililor care îşi
|
||||
generează fişierul xml din aplicaţiile informatice
|
||||
proprii
|
||||
62
tools/anaf-monitor/versions.json
Normal file
62
tools/anaf-monitor/versions.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"D100": {
|
||||
"soft_a_url": "http://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D100_710_XML_0126_100226.pdf",
|
||||
"soft_a_date": "10.02.2026",
|
||||
"soft_j_url": "http://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D100_22012026.zip",
|
||||
"soft_j_date": "22.01.2026"
|
||||
},
|
||||
"D101": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D101_XML_2025_260126.pdf",
|
||||
"soft_a_date": "26.01.2026",
|
||||
"soft_j_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D101_J1102.zip"
|
||||
},
|
||||
"D300": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D300_v12.0.2_12022026.pdf",
|
||||
"soft_a_date": "12.02.2026",
|
||||
"soft_j_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D300Validator_11022026_2.zip",
|
||||
"soft_j_date": "11.02.2026"
|
||||
},
|
||||
"D390": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D390_XML_2020_300424.pdf",
|
||||
"soft_a_date": "30.04.2024",
|
||||
"soft_j_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D390_20250625.zip",
|
||||
"soft_j_date": "25.06.2025"
|
||||
},
|
||||
"D394": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D394_26092025.pdf",
|
||||
"soft_a_date": "26.09.2025",
|
||||
"soft_j_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D394_17092025.zip",
|
||||
"soft_j_date": "17.09.2025"
|
||||
},
|
||||
"D205": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D205_XML_2025_150126.pdf",
|
||||
"soft_a_date": "15.01.2026",
|
||||
"soft_j_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D205_J901_P400.zip"
|
||||
},
|
||||
"D406": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/R405_XML_2017_080321.pdf",
|
||||
"soft_a_date": "08.03.2021",
|
||||
"soft_j_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/D406_20260211.zip",
|
||||
"soft_j_date": "11.02.2026"
|
||||
},
|
||||
"BILANT_2025": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/bilant_SC_1225_XML_110226.pdf",
|
||||
"soft_a_date": "11.02.2026",
|
||||
"soft_j_S1002_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/S1002_20260128.zip",
|
||||
"soft_j_S1002_date": "28.01.2026",
|
||||
"soft_j_S1004_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/S1004_20250204.zip",
|
||||
"soft_j_S1004_date": "04.02.2025",
|
||||
"soft_j_S1003_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/S1003_20260210.zip",
|
||||
"soft_j_S1003_date": "10.02.2026",
|
||||
"soft_j_S1005_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/S1005_202060203.zip"
|
||||
},
|
||||
"SIT_FIN_SEM_2025": {
|
||||
"soft_j_1012_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/S1012_20250723.zip",
|
||||
"soft_j_1012_date": "23.07.2025"
|
||||
},
|
||||
"SIT_FIN_AN_2025": {
|
||||
"soft_a_url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/AplicatiiDec/bilant_S1030_XML_consolidare_270126_bis.pdf",
|
||||
"soft_a_date": "27.01.2026"
|
||||
},
|
||||
"DESCARCARE_DECLARATII": {}
|
||||
}
|
||||
20
tools/backup_config.sh
Executable file
20
tools/backup_config.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Backup config cu retenție: 1 zilnic, 1 săptămânal, 1 lunar
|
||||
|
||||
BACKUP_DIR="/home/moltbot/backups"
|
||||
CONFIG="$HOME/.clawdbot/clawdbot.json"
|
||||
|
||||
# Backup zilnic (suprascrie)
|
||||
cp "$CONFIG" "$BACKUP_DIR/clawdbot-daily.json"
|
||||
|
||||
# Backup săptămânal (duminică)
|
||||
if [ "$(date +%u)" -eq 7 ]; then
|
||||
cp "$CONFIG" "$BACKUP_DIR/clawdbot-weekly.json"
|
||||
fi
|
||||
|
||||
# Backup lunar (ziua 1)
|
||||
if [ "$(date +%d)" -eq 01 ]; then
|
||||
cp "$CONFIG" "$BACKUP_DIR/clawdbot-monthly.json"
|
||||
fi
|
||||
|
||||
echo "Backup done: $(date)"
|
||||
66
tools/calendar_auth.py
Normal file
66
tools/calendar_auth.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Google Calendar OAuth2 Authorization.
|
||||
Run this once to generate token.json for calendar access.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from google_auth_oauthlib.flow import Flow
|
||||
from google.auth.transport.requests import Request
|
||||
from google.oauth2.credentials import Credentials
|
||||
|
||||
# Scopes needed for calendar access (read + write events)
|
||||
SCOPES = ['https://www.googleapis.com/auth/calendar.events']
|
||||
|
||||
CREDENTIALS_FILE = Path(__file__).parent.parent / 'credentials' / 'google-calendar.json'
|
||||
TOKEN_FILE = Path(__file__).parent.parent / 'credentials' / 'google-calendar-token.json'
|
||||
|
||||
def main():
|
||||
creds = None
|
||||
|
||||
# Check if token already exists
|
||||
if TOKEN_FILE.exists():
|
||||
creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES)
|
||||
|
||||
# If no valid credentials, do the OAuth flow
|
||||
if not creds or not creds.valid:
|
||||
if creds and creds.expired and creds.refresh_token:
|
||||
print("Refreshing expired token...")
|
||||
creds.refresh(Request())
|
||||
else:
|
||||
print(f"Starting OAuth flow...")
|
||||
print(f"Using credentials: {CREDENTIALS_FILE}\n")
|
||||
|
||||
flow = Flow.from_client_secrets_file(
|
||||
str(CREDENTIALS_FILE),
|
||||
scopes=SCOPES,
|
||||
redirect_uri='urn:ietf:wg:oauth:2.0:oob'
|
||||
)
|
||||
|
||||
auth_url, _ = flow.authorization_url(prompt='consent')
|
||||
|
||||
print("="*60)
|
||||
print("AUTHORIZATION REQUIRED")
|
||||
print("="*60)
|
||||
print("\n1. Open this URL in your browser:\n")
|
||||
print(auth_url)
|
||||
print("\n2. Sign in and authorize access")
|
||||
print("3. Copy the authorization code and paste it below\n")
|
||||
|
||||
code = input("Enter authorization code: ").strip()
|
||||
|
||||
flow.fetch_token(code=code)
|
||||
creds = flow.credentials
|
||||
|
||||
# Save the credentials for next run
|
||||
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(TOKEN_FILE, 'w') as token:
|
||||
token.write(creds.to_json())
|
||||
print(f"\nToken saved to: {TOKEN_FILE}")
|
||||
|
||||
print("\n✅ Authorization successful!")
|
||||
print("You can now use the calendar tools.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
304
tools/calendar_check.py
Normal file
304
tools/calendar_check.py
Normal file
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Google Calendar checker for Echo.
|
||||
Returns events for today, tomorrow, this week, or upcoming travel needs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
TOKEN_FILE = Path(__file__).parent.parent / 'credentials' / 'google-calendar-token.json'
|
||||
TZ = ZoneInfo('Europe/Bucharest')
|
||||
|
||||
# Keywords that indicate travel to București (needs train + accommodation)
|
||||
TRAVEL_KEYWORDS = ['nlp', 'bucuresti', 'bucurești', 'bucharest']
|
||||
|
||||
def get_service():
|
||||
"""Get authenticated Calendar service."""
|
||||
creds = Credentials.from_authorized_user_file(str(TOKEN_FILE))
|
||||
return build('calendar', 'v3', credentials=creds)
|
||||
|
||||
def get_events(service, time_min, time_max, max_results=20):
|
||||
"""Get events between time_min and time_max."""
|
||||
events_result = service.events().list(
|
||||
calendarId='primary',
|
||||
timeMin=time_min.isoformat(),
|
||||
timeMax=time_max.isoformat(),
|
||||
maxResults=max_results,
|
||||
singleEvents=True,
|
||||
orderBy='startTime'
|
||||
).execute()
|
||||
return events_result.get('items', [])
|
||||
|
||||
def format_event(event):
|
||||
"""Format event for display."""
|
||||
start = event['start'].get('dateTime', event['start'].get('date'))
|
||||
summary = event.get('summary', '(fără titlu)')
|
||||
|
||||
# Parse start time
|
||||
if 'T' in start:
|
||||
dt = datetime.fromisoformat(start)
|
||||
time_str = dt.strftime('%H:%M')
|
||||
date_str = dt.strftime('%d %b')
|
||||
else:
|
||||
time_str = 'toată ziua'
|
||||
date_str = datetime.fromisoformat(start).strftime('%d %b')
|
||||
|
||||
return {
|
||||
'summary': summary,
|
||||
'date': date_str,
|
||||
'time': time_str,
|
||||
'start': start,
|
||||
'is_travel': any(kw in summary.lower() for kw in TRAVEL_KEYWORDS)
|
||||
}
|
||||
|
||||
def check_today_tomorrow():
|
||||
"""Get events for today and tomorrow."""
|
||||
service = get_service()
|
||||
now = datetime.now(TZ)
|
||||
|
||||
# Today
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_end = today_start + timedelta(days=1)
|
||||
|
||||
# Tomorrow
|
||||
tomorrow_end = today_end + timedelta(days=1)
|
||||
|
||||
today_events = get_events(service, today_start, today_end)
|
||||
tomorrow_events = get_events(service, today_end, tomorrow_end)
|
||||
|
||||
result = {
|
||||
'today': [format_event(e) for e in today_events],
|
||||
'tomorrow': [format_event(e) for e in tomorrow_events]
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def check_week():
|
||||
"""Get events for this week (Mon-Sun)."""
|
||||
service = get_service()
|
||||
now = datetime.now(TZ)
|
||||
|
||||
# Start of this week (Monday)
|
||||
days_since_monday = now.weekday()
|
||||
week_start = now.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=days_since_monday)
|
||||
week_end = week_start + timedelta(days=7)
|
||||
|
||||
events = get_events(service, week_start, week_end)
|
||||
|
||||
return {
|
||||
'week_start': week_start.strftime('%d %b'),
|
||||
'week_end': (week_end - timedelta(days=1)).strftime('%d %b'),
|
||||
'events': [format_event(e) for e in events]
|
||||
}
|
||||
|
||||
def check_travel_upcoming():
|
||||
"""Check for travel events in next 14 days that need booking (7-11 days out)."""
|
||||
service = get_service()
|
||||
now = datetime.now(TZ)
|
||||
|
||||
# Look 14 days ahead
|
||||
future = now + timedelta(days=14)
|
||||
events = get_events(service, now, future)
|
||||
|
||||
reminders = []
|
||||
for event in events:
|
||||
formatted = format_event(event)
|
||||
if formatted['is_travel']:
|
||||
# Calculate days until event
|
||||
start_str = event['start'].get('dateTime', event['start'].get('date'))
|
||||
if 'T' in start_str:
|
||||
event_date = datetime.fromisoformat(start_str).date()
|
||||
else:
|
||||
event_date = datetime.fromisoformat(start_str).date()
|
||||
|
||||
days_until = (event_date - now.date()).days
|
||||
|
||||
# Remind if 7-11 days away (booking window)
|
||||
if 7 <= days_until <= 11:
|
||||
reminders.append({
|
||||
**formatted,
|
||||
'days_until': days_until,
|
||||
'action': 'Cumpără bilete tren + asigură cazare București'
|
||||
})
|
||||
# Urgent if 3-6 days away and might have missed window
|
||||
elif 3 <= days_until <= 6:
|
||||
reminders.append({
|
||||
**formatted,
|
||||
'days_until': days_until,
|
||||
'action': '⚠️ URGENT: Verifică dacă ai bilete și cazare!'
|
||||
})
|
||||
|
||||
return {'travel_reminders': reminders}
|
||||
|
||||
def is_busy_now():
|
||||
"""Check if there's an event happening RIGHT NOW."""
|
||||
service = get_service()
|
||||
now = datetime.now(TZ)
|
||||
|
||||
# Check events that started before now and end after now
|
||||
events_result = service.events().list(
|
||||
calendarId='primary',
|
||||
timeMin=(now - timedelta(hours=4)).isoformat(),
|
||||
timeMax=(now + timedelta(minutes=30)).isoformat(),
|
||||
singleEvents=True,
|
||||
orderBy='startTime'
|
||||
).execute()
|
||||
|
||||
for event in events_result.get('items', []):
|
||||
start_str = event['start'].get('dateTime', event['start'].get('date'))
|
||||
end_str = event['end'].get('dateTime', event['end'].get('date'))
|
||||
|
||||
# Skip all-day events for "busy now" check
|
||||
if 'T' not in start_str:
|
||||
continue
|
||||
|
||||
start = datetime.fromisoformat(start_str)
|
||||
end = datetime.fromisoformat(end_str)
|
||||
|
||||
if start <= now <= end:
|
||||
return {
|
||||
'busy': True,
|
||||
'event': event.get('summary', '(fără titlu)'),
|
||||
'ends': end.strftime('%H:%M')
|
||||
}
|
||||
|
||||
return {'busy': False}
|
||||
|
||||
def check_upcoming_hours(hours=2):
|
||||
"""Check for events in the next N hours."""
|
||||
service = get_service()
|
||||
now = datetime.now(TZ)
|
||||
future = now + timedelta(hours=hours)
|
||||
|
||||
events = get_events(service, now, future)
|
||||
|
||||
alerts = []
|
||||
for event in events:
|
||||
start_str = event['start'].get('dateTime', event['start'].get('date'))
|
||||
summary = event.get('summary', '(fără titlu)')
|
||||
|
||||
if 'T' in start_str:
|
||||
start = datetime.fromisoformat(start_str)
|
||||
minutes_until = int((start - now).total_seconds() / 60)
|
||||
if minutes_until > 0:
|
||||
alerts.append({
|
||||
'summary': summary,
|
||||
'minutes_until': minutes_until,
|
||||
'time': start.strftime('%H:%M')
|
||||
})
|
||||
|
||||
return {'upcoming': alerts}
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: calendar_check.py [today|week|travel|busy|soon|all]")
|
||||
sys.exit(1)
|
||||
|
||||
mode = sys.argv[1].lower()
|
||||
|
||||
if mode == 'today':
|
||||
result = check_today_tomorrow()
|
||||
elif mode == 'week':
|
||||
result = check_week()
|
||||
elif mode == 'travel':
|
||||
result = check_travel_upcoming()
|
||||
elif mode == 'busy':
|
||||
result = is_busy_now()
|
||||
elif mode == 'soon':
|
||||
hours = int(sys.argv[2]) if len(sys.argv) > 2 else 2
|
||||
result = check_upcoming_hours(hours)
|
||||
elif mode == 'all':
|
||||
result = {
|
||||
**check_today_tomorrow(),
|
||||
**check_week(),
|
||||
**check_travel_upcoming()
|
||||
}
|
||||
else:
|
||||
print(f"Unknown mode: {mode}")
|
||||
sys.exit(1)
|
||||
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
def create_event(summary, start_datetime, duration_minutes=60, description=None,
|
||||
reminders=None, is_travel=False):
|
||||
"""
|
||||
Create a calendar event with reminders.
|
||||
|
||||
Args:
|
||||
summary: Event title
|
||||
start_datetime: datetime object or ISO string (e.g., "2026-02-05T15:00:00")
|
||||
duration_minutes: Duration in minutes (default 60)
|
||||
description: Optional description
|
||||
reminders: List of minutes before event for reminders, or None for defaults
|
||||
e.g., [120, 30] = 2 hours and 30 min before
|
||||
is_travel: If True, uses travel reminders (evening before + 2h before)
|
||||
|
||||
Returns:
|
||||
Created event details
|
||||
"""
|
||||
service = get_service()
|
||||
|
||||
# Parse start time if string
|
||||
if isinstance(start_datetime, str):
|
||||
start = datetime.fromisoformat(start_datetime)
|
||||
else:
|
||||
start = start_datetime
|
||||
|
||||
# Ensure timezone
|
||||
if start.tzinfo is None:
|
||||
start = start.replace(tzinfo=TZ)
|
||||
|
||||
end = start + timedelta(minutes=duration_minutes)
|
||||
|
||||
# Set up reminders
|
||||
if reminders is None:
|
||||
if is_travel:
|
||||
# Travel: evening before (calculate minutes to 18:00 day before) + 2h before
|
||||
evening_before = start.replace(hour=18, minute=0, second=0) - timedelta(days=1)
|
||||
minutes_to_evening = int((start - evening_before).total_seconds() / 60)
|
||||
reminders = [minutes_to_evening, 120] # Evening before + 2 hours
|
||||
else:
|
||||
# Default: 30 min before
|
||||
reminders = [30]
|
||||
|
||||
event = {
|
||||
'summary': summary,
|
||||
'start': {
|
||||
'dateTime': start.isoformat(),
|
||||
'timeZone': 'Europe/Bucharest',
|
||||
},
|
||||
'end': {
|
||||
'dateTime': end.isoformat(),
|
||||
'timeZone': 'Europe/Bucharest',
|
||||
},
|
||||
'reminders': {
|
||||
'useDefault': False,
|
||||
'overrides': [
|
||||
{'method': 'popup', 'minutes': m} for m in reminders
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
if description:
|
||||
event['description'] = description
|
||||
|
||||
created = service.events().insert(calendarId='primary', body=event).execute()
|
||||
|
||||
return {
|
||||
'id': created['id'],
|
||||
'summary': created['summary'],
|
||||
'start': created['start'].get('dateTime'),
|
||||
'link': created.get('htmlLink'),
|
||||
'reminders': reminders
|
||||
}
|
||||
179
tools/content_discovery.py
Executable file
179
tools/content_discovery.py
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Content Discovery - Căutare automată de conținut bazată pe interese.
|
||||
Rulează noaptea, pregătește note pentru morning report.
|
||||
|
||||
Usage: python3 content_discovery.py [--dry-run]
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
WORKSPACE = Path(__file__).parent.parent
|
||||
MEMORY_DIR = WORKSPACE / "memory"
|
||||
INSIGHTS_DIR = WORKSPACE / "kb" / "insights"
|
||||
USER_MD = WORKSPACE / "USER.md"
|
||||
|
||||
# Interese de bază (fallback)
|
||||
BASE_INTERESTS = [
|
||||
"NLP Sleight of Mouth patterns",
|
||||
"comunicare nonviolentă Marshall Rosenberg",
|
||||
"James Clear atomic habits productivity",
|
||||
"Monica Ion mindset antreprenor",
|
||||
"dezvoltare personală coaching",
|
||||
"Rumi quotes wisdom philosophy",
|
||||
"stoicism practical philosophy",
|
||||
"noua medicină germanică",
|
||||
"post negru fasting benefits",
|
||||
"80/20 principle productivity",
|
||||
"leadership entrepreneurship",
|
||||
]
|
||||
|
||||
def get_recent_files(directory: Path, days: int = 3) -> list:
|
||||
"""Get files modified in last N days"""
|
||||
cutoff = datetime.now() - timedelta(days=days)
|
||||
files = []
|
||||
if directory.exists():
|
||||
for f in directory.glob("*.md"):
|
||||
if f.stat().st_mtime > cutoff.timestamp():
|
||||
files.append(f)
|
||||
return sorted(files, key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
|
||||
def extract_topics_from_file(filepath: Path) -> list:
|
||||
"""Extract potential topics/keywords from a markdown file"""
|
||||
topics = []
|
||||
try:
|
||||
content = filepath.read_text(encoding='utf-8')
|
||||
|
||||
# Extract from headers
|
||||
headers = re.findall(r'^##?\s+(.+)$', content, re.MULTILINE)
|
||||
topics.extend(headers[:5])
|
||||
|
||||
# Extract YouTube video titles
|
||||
yt_titles = re.findall(r'^#\s+(.+)$', content, re.MULTILINE)
|
||||
topics.extend(yt_titles[:3])
|
||||
|
||||
# Extract @tags
|
||||
tags = re.findall(r'@(\w+)', content)
|
||||
topics.extend(tags[:5])
|
||||
|
||||
# Extract bold terms
|
||||
bold = re.findall(r'\*\*([^*]+)\*\*', content)
|
||||
topics.extend([b for b in bold if len(b) < 50][:5])
|
||||
|
||||
except Exception as e:
|
||||
print(f" Warning: Could not read {filepath}: {e}")
|
||||
|
||||
return list(set(topics))
|
||||
|
||||
def get_recent_topics() -> list:
|
||||
"""Analyze recent memory and insights to find current interests"""
|
||||
recent_topics = []
|
||||
|
||||
# Check recent memory files
|
||||
print("Scanning recent memory...")
|
||||
for f in get_recent_files(MEMORY_DIR, days=3):
|
||||
topics = extract_topics_from_file(f)
|
||||
recent_topics.extend(topics)
|
||||
print(f" {f.name}: {len(topics)} topics")
|
||||
|
||||
# Check recent insights
|
||||
print("Scanning recent insights...")
|
||||
for f in get_recent_files(INSIGHTS_DIR, days=3):
|
||||
topics = extract_topics_from_file(f)
|
||||
recent_topics.extend(topics)
|
||||
print(f" {f.name}: {len(topics)} topics")
|
||||
|
||||
# Check recent YouTube notes
|
||||
yt_dir = WORKSPACE / "kb" / "youtube"
|
||||
print("Scanning recent YouTube notes...")
|
||||
for f in get_recent_files(yt_dir, days=3):
|
||||
topics = extract_topics_from_file(f)
|
||||
recent_topics.extend(topics)
|
||||
print(f" {f.name}: {len(topics)} topics")
|
||||
|
||||
return list(set(recent_topics))
|
||||
|
||||
def build_search_queries(recent_topics: list, base_interests: list) -> list:
|
||||
"""Build search queries with 60% recent, 40% base interests"""
|
||||
queries = []
|
||||
|
||||
# Filter and clean topics
|
||||
recent_clean = [t for t in recent_topics if len(t) > 3 and len(t) < 100][:10]
|
||||
|
||||
# 60% from recent (if available)
|
||||
if recent_clean:
|
||||
# Pick top 2-3 recent topics
|
||||
for topic in recent_clean[:3]:
|
||||
queries.append({
|
||||
"query": f"{topic} YouTube tutorial",
|
||||
"source": "recent",
|
||||
"topic": topic
|
||||
})
|
||||
|
||||
# 40% from base interests (rotate based on day)
|
||||
day_of_year = datetime.now().timetuple().tm_yday
|
||||
rotated_base = base_interests[day_of_year % len(base_interests):]
|
||||
rotated_base.extend(base_interests[:day_of_year % len(base_interests)])
|
||||
|
||||
for interest in rotated_base[:2]:
|
||||
queries.append({
|
||||
"query": interest,
|
||||
"source": "base",
|
||||
"topic": interest
|
||||
})
|
||||
|
||||
return queries[:5] # Max 5 queries
|
||||
|
||||
def save_discovery_plan(queries: list):
|
||||
"""Save the discovery plan for the agent to execute"""
|
||||
plan = {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"queries": queries,
|
||||
"status": "pending",
|
||||
"results": []
|
||||
}
|
||||
|
||||
plan_file = WORKSPACE / "memory" / "content-discovery-plan.json"
|
||||
with open(plan_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(plan, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n✅ Plan saved to {plan_file}")
|
||||
return plan_file
|
||||
|
||||
def main(dry_run: bool = False):
|
||||
print("=" * 50)
|
||||
print("🔍 Content Discovery - Pregătire căutare")
|
||||
print(f" Data: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
print("=" * 50)
|
||||
|
||||
# 1. Get recent topics
|
||||
recent_topics = get_recent_topics()
|
||||
print(f"\n📌 Topics recente găsite: {len(recent_topics)}")
|
||||
if recent_topics:
|
||||
print(f" Exemple: {recent_topics[:5]}")
|
||||
|
||||
# 2. Build search queries
|
||||
queries = build_search_queries(recent_topics, BASE_INTERESTS)
|
||||
print(f"\n🔎 Queries generate: {len(queries)}")
|
||||
for i, q in enumerate(queries, 1):
|
||||
print(f" {i}. [{q['source']}] {q['query'][:60]}...")
|
||||
|
||||
if dry_run:
|
||||
print("\n⚠️ DRY RUN - nu salvez planul")
|
||||
return
|
||||
|
||||
# 3. Save plan for agent execution
|
||||
plan_file = save_discovery_plan(queries)
|
||||
|
||||
print("\n📋 Următorul pas:")
|
||||
print(" Agentul va citi planul și va executa căutările")
|
||||
print(" Rezultatele vor fi în morning report")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
main(dry_run)
|
||||
101
tools/email_check.py
Normal file
101
tools/email_check.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IMAP inbox checker for moltbot@romfast.ro
|
||||
Returns unread emails as JSON
|
||||
"""
|
||||
|
||||
import imaplib
|
||||
import email
|
||||
from email.header import decode_header
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# IMAP Configuration
|
||||
IMAP_SERVER = "mail.romfast.ro"
|
||||
IMAP_PORT = 993
|
||||
IMAP_USER = "moltbot@romfast.ro"
|
||||
IMAP_PASS = "parola281234"
|
||||
|
||||
def decode_mime_header(header):
|
||||
"""Decode MIME encoded header"""
|
||||
if not header:
|
||||
return ""
|
||||
decoded = decode_header(header)
|
||||
result = []
|
||||
for part, encoding in decoded:
|
||||
if isinstance(part, bytes):
|
||||
result.append(part.decode(encoding or 'utf-8', errors='replace'))
|
||||
else:
|
||||
result.append(part)
|
||||
return ''.join(result)
|
||||
|
||||
def get_email_body(msg):
|
||||
"""Extract email body text"""
|
||||
body = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
if content_type == "text/plain":
|
||||
try:
|
||||
body = part.get_payload(decode=True).decode('utf-8', errors='replace')
|
||||
break
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
body = msg.get_payload(decode=True).decode('utf-8', errors='replace')
|
||||
except:
|
||||
pass
|
||||
return body[:2000] # Limit body length
|
||||
|
||||
def check_inbox(unread_only=True, limit=10):
|
||||
"""Check inbox and return emails"""
|
||||
try:
|
||||
# Connect to IMAP
|
||||
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
|
||||
mail.login(IMAP_USER, IMAP_PASS)
|
||||
mail.select("INBOX")
|
||||
|
||||
# Search for emails
|
||||
criteria = "UNSEEN" if unread_only else "ALL"
|
||||
status, messages = mail.search(None, criteria)
|
||||
|
||||
if status != "OK":
|
||||
return {"ok": False, "error": "Search failed"}
|
||||
|
||||
email_ids = messages[0].split()
|
||||
email_ids = email_ids[-limit:] # Get last N
|
||||
|
||||
emails = []
|
||||
for eid in reversed(email_ids): # Newest first
|
||||
status, msg_data = mail.fetch(eid, "(RFC822)")
|
||||
if status != "OK":
|
||||
continue
|
||||
|
||||
raw_email = msg_data[0][1]
|
||||
msg = email.message_from_bytes(raw_email)
|
||||
|
||||
emails.append({
|
||||
"id": eid.decode(),
|
||||
"from": decode_mime_header(msg["From"]),
|
||||
"subject": decode_mime_header(msg["Subject"]),
|
||||
"date": msg["Date"],
|
||||
"body_preview": get_email_body(msg)[:500]
|
||||
})
|
||||
|
||||
mail.logout()
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"unread_count": len(emails),
|
||||
"emails": emails
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
if __name__ == "__main__":
|
||||
unread = "--all" not in sys.argv
|
||||
result = check_inbox(unread_only=unread)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
238
tools/email_process.py
Executable file
238
tools/email_process.py
Executable file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Process emails from echo@romfast.ro inbox.
|
||||
Saves emails as notes in memory/kb/emails/ for further insight extraction.
|
||||
|
||||
Usage:
|
||||
python3 email_process.py # List unread emails
|
||||
python3 email_process.py --save # Save unread emails as notes
|
||||
python3 email_process.py --all # List all emails
|
||||
"""
|
||||
|
||||
import imaplib
|
||||
import email
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
from email.header import decode_header
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Load .env
|
||||
env_path = Path(__file__).parent.parent / '.env'
|
||||
if env_path.exists():
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
# Config
|
||||
IMAP_SERVER = os.environ.get('EMAIL_SERVER', 'mail.romfast.ro')
|
||||
IMAP_PORT = 993
|
||||
IMAP_USER = os.environ.get('EMAIL_USER', 'echo@romfast.ro')
|
||||
IMAP_PASS = os.environ.get('EMAIL_PASSWORD', '')
|
||||
|
||||
# Whitelist - only process emails from these addresses
|
||||
WHITELIST = [
|
||||
'mmarius28@gmail.com',
|
||||
'marius.mutu@romfast.ro',
|
||||
]
|
||||
|
||||
KB_PATH = Path(__file__).parent.parent / 'kb' / 'emails'
|
||||
|
||||
def slugify(text: str, max_len: int = 50) -> str:
|
||||
"""Convert text to URL-friendly slug"""
|
||||
text = text.lower()
|
||||
text = re.sub(r'[^\w\s-]', '', text)
|
||||
text = re.sub(r'[\s_]+', '-', text)
|
||||
text = re.sub(r'-+', '-', text).strip('-')
|
||||
return text[:max_len]
|
||||
|
||||
def decode_mime_header(header):
|
||||
"""Decode MIME encoded header"""
|
||||
if not header:
|
||||
return ""
|
||||
decoded_parts = []
|
||||
for part, encoding in decode_header(header):
|
||||
if isinstance(part, bytes):
|
||||
decoded_parts.append(part.decode(encoding or 'utf-8', errors='replace'))
|
||||
else:
|
||||
decoded_parts.append(part)
|
||||
return ' '.join(decoded_parts)
|
||||
|
||||
def get_email_body(msg):
|
||||
"""Extract plain text body from email"""
|
||||
body = ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
if content_type == "text/plain":
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
body = payload.decode(charset, errors='replace')
|
||||
break
|
||||
elif content_type == "text/html" and not body:
|
||||
# Fallback to HTML if no plain text
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
body = payload.decode(charset, errors='replace')
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = msg.get_content_charset() or 'utf-8'
|
||||
body = payload.decode(charset, errors='replace')
|
||||
return body.strip()
|
||||
|
||||
def extract_sender_email(from_header: str) -> str:
|
||||
"""Extract just the email address from From header"""
|
||||
match = re.search(r'<([^>]+)>', from_header)
|
||||
if match:
|
||||
return match.group(1).lower()
|
||||
return from_header.lower().strip()
|
||||
|
||||
def list_emails(show_all=False):
|
||||
"""List emails in inbox"""
|
||||
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
|
||||
mail.login(IMAP_USER, IMAP_PASS)
|
||||
mail.select('INBOX')
|
||||
|
||||
search_criteria = 'ALL' if show_all else 'UNSEEN'
|
||||
status, messages = mail.search(None, search_criteria)
|
||||
|
||||
email_ids = messages[0].split() if messages[0] else []
|
||||
emails = []
|
||||
|
||||
for eid in email_ids:
|
||||
status, data = mail.fetch(eid, '(RFC822)')
|
||||
msg = email.message_from_bytes(data[0][1])
|
||||
|
||||
from_addr = decode_mime_header(msg['From'])
|
||||
sender_email = extract_sender_email(from_addr)
|
||||
subject = decode_mime_header(msg['Subject'])
|
||||
date = msg['Date']
|
||||
|
||||
emails.append({
|
||||
'id': eid.decode(),
|
||||
'from': from_addr,
|
||||
'sender_email': sender_email,
|
||||
'subject': subject,
|
||||
'date': date,
|
||||
'whitelisted': sender_email in WHITELIST
|
||||
})
|
||||
|
||||
mail.logout()
|
||||
return emails
|
||||
|
||||
def save_email_as_note(eid: str) -> dict:
|
||||
"""Save a single email as a markdown note"""
|
||||
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
|
||||
mail.login(IMAP_USER, IMAP_PASS)
|
||||
mail.select('INBOX')
|
||||
|
||||
status, data = mail.fetch(eid.encode(), '(RFC822)')
|
||||
msg = email.message_from_bytes(data[0][1])
|
||||
|
||||
from_addr = decode_mime_header(msg['From'])
|
||||
sender_email = extract_sender_email(from_addr)
|
||||
subject = decode_mime_header(msg['Subject'])
|
||||
date_str = msg['Date']
|
||||
body = get_email_body(msg)
|
||||
|
||||
# Check whitelist
|
||||
if sender_email not in WHITELIST:
|
||||
mail.logout()
|
||||
return {'ok': False, 'error': f'Sender {sender_email} not in whitelist'}
|
||||
|
||||
# Parse date
|
||||
try:
|
||||
# Try common date formats
|
||||
for fmt in ['%a, %d %b %Y %H:%M:%S %z', '%d %b %Y %H:%M:%S %z']:
|
||||
try:
|
||||
parsed_date = datetime.strptime(date_str.split(' (')[0].strip(), fmt)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
parsed_date = datetime.now()
|
||||
except:
|
||||
parsed_date = datetime.now()
|
||||
|
||||
date_prefix = parsed_date.strftime('%Y-%m-%d')
|
||||
slug = slugify(subject) or 'email'
|
||||
filename = f"{date_prefix}_{slug}.md"
|
||||
filepath = KB_PATH / filename
|
||||
|
||||
# Create markdown note
|
||||
content = f"""# {subject}
|
||||
|
||||
**De la:** {from_addr}
|
||||
**Data:** {date_str}
|
||||
**Salvat:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
---
|
||||
|
||||
{body}
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
<!-- Echo: completează cu rezumat -->
|
||||
|
||||
## Insights
|
||||
<!-- Echo: extrage idei acționabile cu tag-uri @work @health @growth etc -->
|
||||
"""
|
||||
|
||||
KB_PATH.mkdir(parents=True, exist_ok=True)
|
||||
filepath.write_text(content, encoding='utf-8')
|
||||
|
||||
# Mark as seen
|
||||
mail.store(eid.encode(), '+FLAGS', '\\Seen')
|
||||
mail.logout()
|
||||
|
||||
return {
|
||||
'ok': True,
|
||||
'file': str(filepath),
|
||||
'subject': subject,
|
||||
'from': sender_email
|
||||
}
|
||||
|
||||
def save_unread_emails():
|
||||
"""Save all unread whitelisted emails as notes"""
|
||||
emails = list_emails(show_all=False)
|
||||
results = []
|
||||
|
||||
for em in emails:
|
||||
if em['whitelisted']:
|
||||
result = save_email_as_note(em['id'])
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
if __name__ == "__main__":
|
||||
if '--save' in sys.argv:
|
||||
results = save_unread_emails()
|
||||
for r in results:
|
||||
if r['ok']:
|
||||
print(f"✅ Salvat: {r['file']}")
|
||||
else:
|
||||
print(f"❌ Eroare: {r['error']}")
|
||||
if not results:
|
||||
print("Niciun email nou de la adrese whitelisted.")
|
||||
else:
|
||||
show_all = '--all' in sys.argv
|
||||
emails = list_emails(show_all=show_all)
|
||||
|
||||
if not emails:
|
||||
print("Inbox gol." if show_all else "Niciun email necitit.")
|
||||
else:
|
||||
for em in emails:
|
||||
wl = "✅" if em['whitelisted'] else "⚠️"
|
||||
print(f"{wl} [{em['id']}] {em['subject']}")
|
||||
print(f" De la: {em['from']}")
|
||||
print(f" Data: {em['date']}")
|
||||
print()
|
||||
86
tools/email_send.py
Normal file
86
tools/email_send.py
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple SMTP email sender for echo@romfast.ro
|
||||
Usage: python3 email_send.py "recipient@email.com" "Subject" "Body text"
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
import ssl
|
||||
import sys
|
||||
import os
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.header import Header
|
||||
from email.utils import formataddr
|
||||
from pathlib import Path
|
||||
|
||||
# Load .env file
|
||||
env_path = Path(__file__).parent.parent / '.env'
|
||||
if env_path.exists():
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
# SMTP Configuration from environment
|
||||
# Try Gmail first, fall back to romfast
|
||||
if os.environ.get('GMAIL_PASSWORD'):
|
||||
SMTP_SERVER = 'smtp.gmail.com'
|
||||
SMTP_PORT = 465
|
||||
SMTP_USER = os.environ.get('GMAIL_USER', 'mmarius28@gmail.com')
|
||||
SMTP_PASS = os.environ.get('GMAIL_PASSWORD', '')
|
||||
else:
|
||||
SMTP_SERVER = os.environ.get('EMAIL_SERVER', 'mail.romfast.ro')
|
||||
SMTP_PORT = 465
|
||||
SMTP_USER = os.environ.get('EMAIL_USER', 'echo@romfast.ro')
|
||||
SMTP_PASS = os.environ.get('EMAIL_PASSWORD', '')
|
||||
FROM_NAME = "Echo"
|
||||
|
||||
def send_email(to_email: str, subject: str, body: str, html: bool = False) -> dict:
|
||||
"""Send an email via SMTP SSL"""
|
||||
try:
|
||||
# Create message
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = Header(subject, 'utf-8')
|
||||
msg["From"] = formataddr((FROM_NAME, SMTP_USER))
|
||||
msg["To"] = to_email
|
||||
msg["Reply-To"] = "echo@romfast.ro"
|
||||
|
||||
# Attach body
|
||||
if html:
|
||||
msg.attach(MIMEText(body, "html", "utf-8"))
|
||||
else:
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
|
||||
# Connect and send
|
||||
context = ssl.create_default_context()
|
||||
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context) as server:
|
||||
server.login(SMTP_USER, SMTP_PASS)
|
||||
server.sendmail(SMTP_USER, to_email, msg.as_string())
|
||||
|
||||
return {"ok": True, "to": to_email, "subject": subject}
|
||||
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python3 email_send.py <to> <subject> <body> [--html]")
|
||||
sys.exit(1)
|
||||
|
||||
to = sys.argv[1]
|
||||
subject = sys.argv[2]
|
||||
body = sys.argv[3]
|
||||
|
||||
# Auto-detect HTML or use --html flag
|
||||
# Check for common HTML patterns, not just doctype/html tags
|
||||
body_lower = body.strip().lower()
|
||||
has_html_tags = any(tag in body_lower for tag in ['<html', '<!doctype', '<div', '<p>', '<br', '<table', '<h1', '<h2', '<h3', '<span', '<style'])
|
||||
is_html = "--html" in sys.argv or has_html_tags
|
||||
|
||||
result = send_email(to, subject, body, html=is_html)
|
||||
|
||||
import json
|
||||
print(json.dumps(result))
|
||||
113
tools/generate_pdf.py
Normal file
113
tools/generate_pdf.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate PDF from markdown content.
|
||||
Outputs PDF to stdout as binary.
|
||||
Simple, robust approach focusing on text content.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Read JSON from stdin
|
||||
input_data = json.load(sys.stdin)
|
||||
markdown_content = input_data.get('markdown', '')
|
||||
filename = input_data.get('filename', 'document.pdf')
|
||||
|
||||
try:
|
||||
from fpdf import FPDF
|
||||
import re
|
||||
|
||||
# Create PDF
|
||||
pdf = FPDF(format='A4')
|
||||
pdf.add_page()
|
||||
pdf.set_margins(12, 12, 12)
|
||||
|
||||
# Try to use DejaVu font for Romanian support
|
||||
try:
|
||||
dejavu_path = Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
|
||||
if dejavu_path.exists():
|
||||
pdf.add_font("DejaVu", "", str(dejavu_path))
|
||||
pdf.add_font("DejaVu", "B", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
|
||||
pdf.set_font("DejaVu", "", 10)
|
||||
use_dejavu = True
|
||||
else:
|
||||
raise Exception("DejaVu font not found")
|
||||
except:
|
||||
pdf.set_font("Helvetica", "", 10)
|
||||
use_dejavu = False
|
||||
|
||||
# Parse markdown line by line
|
||||
lines = markdown_content.split('\n')
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Skip empty lines but add spacing
|
||||
if not line.strip():
|
||||
pdf.ln(2)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# H1 - Main heading
|
||||
if line.startswith('# '):
|
||||
pdf.set_font("DejaVu" if use_dejavu else "Helvetica", "B", 16)
|
||||
text = line.replace('# ', '', 1).strip()
|
||||
pdf.multi_cell(0, 7, text, ln=True)
|
||||
pdf.ln(1)
|
||||
pdf.set_font("DejaVu" if use_dejavu else "Helvetica", "", 10)
|
||||
|
||||
# H2 - Section heading
|
||||
elif line.startswith('## '):
|
||||
pdf.set_font("DejaVu" if use_dejavu else "Helvetica", "B", 12)
|
||||
text = line.replace('## ', '', 1).strip()
|
||||
pdf.multi_cell(0, 6, text, ln=True)
|
||||
pdf.ln(0.5)
|
||||
pdf.set_font("DejaVu" if use_dejavu else "Helvetica", "", 10)
|
||||
|
||||
# H3 - Subsection
|
||||
elif line.startswith('### '):
|
||||
pdf.set_font("DejaVu" if use_dejavu else "Helvetica", "B", 11)
|
||||
text = line.replace('### ', '', 1).strip()
|
||||
pdf.multi_cell(0, 5, text, ln=True)
|
||||
pdf.ln(0.3)
|
||||
pdf.set_font("DejaVu" if use_dejavu else "Helvetica", "", 10)
|
||||
|
||||
# Bullet point
|
||||
elif line.strip().startswith('- ') or line.strip().startswith('* '):
|
||||
text = line.strip().lstrip('-*').strip()
|
||||
# Use simple dash for bullet
|
||||
pdf.multi_cell(0, 5, '- ' + text, ln=True)
|
||||
|
||||
# Numbered list
|
||||
elif re.match(r'^\s*\d+\.\s', line):
|
||||
text = re.sub(r'^\s*\d+\.\s', '', line)
|
||||
pdf.multi_cell(0, 5, text, ln=True)
|
||||
|
||||
# Regular text with formatting
|
||||
else:
|
||||
# Clean up markdown markers but keep structure
|
||||
text = line.strip()
|
||||
|
||||
# Remove inline markdown
|
||||
text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) # Bold
|
||||
text = re.sub(r'__(.*?)__', r'\1', text) # Bold
|
||||
text = re.sub(r'\*(.*?)\*', r'\1', text) # Italic
|
||||
text = re.sub(r'_(.*?)_', r'\1', text) # Italic
|
||||
text = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', text) # Links
|
||||
|
||||
if text:
|
||||
pdf.multi_cell(0, 5, text, ln=True)
|
||||
|
||||
i += 1
|
||||
|
||||
# Output PDF
|
||||
pdf_bytes = pdf.output()
|
||||
sys.stdout.buffer.write(pdf_bytes)
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
error_json = json.dumps({'error': str(e)})
|
||||
sys.stderr.write(error_json)
|
||||
sys.exit(1)
|
||||
96
tools/git_commit.py
Executable file
96
tools/git_commit.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Git commit helper - auto-generates commit message from changed files
|
||||
Usage: python3 git_commit.py [--push] [--dry-run]
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
REPO_PATH = os.path.expanduser("~/clawd")
|
||||
|
||||
def run(cmd, capture=True):
|
||||
result = subprocess.run(cmd, shell=True, cwd=REPO_PATH,
|
||||
capture_output=capture, text=True)
|
||||
return result.stdout.strip() if capture else result.returncode
|
||||
|
||||
def get_status():
|
||||
"""Get git status summary"""
|
||||
status = run("git status --porcelain")
|
||||
if not status:
|
||||
return None, []
|
||||
|
||||
files = []
|
||||
for line in status.split('\n'):
|
||||
if line.strip():
|
||||
status_code = line[:2]
|
||||
filename = line[3:]
|
||||
files.append((status_code.strip(), filename))
|
||||
return status, files
|
||||
|
||||
def generate_message(files):
|
||||
"""Generate commit message from changed files"""
|
||||
# Group by directory/type
|
||||
areas = set()
|
||||
for _, f in files:
|
||||
if '/' in f:
|
||||
areas.add(f.split('/')[0])
|
||||
else:
|
||||
areas.add('root')
|
||||
|
||||
# Count changes
|
||||
added = len([f for s, f in files if 'A' in s or '?' in s])
|
||||
modified = len([f for s, f in files if 'M' in s])
|
||||
deleted = len([f for s, f in files if 'D' in s])
|
||||
|
||||
parts = []
|
||||
if added: parts.append(f"+{added}")
|
||||
if modified: parts.append(f"~{modified}")
|
||||
if deleted: parts.append(f"-{deleted}")
|
||||
|
||||
change_summary = " ".join(parts) if parts else "changes"
|
||||
area_list = ", ".join(sorted(areas)[:3])
|
||||
if len(areas) > 3:
|
||||
area_list += f" +{len(areas)-3} more"
|
||||
|
||||
return f"Update {area_list} ({change_summary})"
|
||||
|
||||
def main():
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
push = "--push" in sys.argv
|
||||
|
||||
status, files = get_status()
|
||||
|
||||
if not status:
|
||||
print('{"status": "clean", "message": "Nothing to commit"}')
|
||||
return 0
|
||||
|
||||
print(f"Files changed: {len(files)}")
|
||||
for s, f in files[:10]:
|
||||
print(f" [{s}] {f}")
|
||||
if len(files) > 10:
|
||||
print(f" ... and {len(files)-10} more")
|
||||
|
||||
message = generate_message(files)
|
||||
print(f"\nCommit message: {message}")
|
||||
|
||||
if dry_run:
|
||||
print("\n[DRY RUN - no changes made]")
|
||||
return 0
|
||||
|
||||
# Stage all and commit
|
||||
run("git add -A", capture=False)
|
||||
result = run(f'git commit -m "{message}"')
|
||||
print(f"\n{result}")
|
||||
|
||||
if push:
|
||||
print("\nPushing...")
|
||||
push_result = run("git push 2>&1")
|
||||
print(push_result)
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
68
tools/lead-gen/README.md
Normal file
68
tools/lead-gen/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Lead Generation Minimal - ROA
|
||||
|
||||
Sistem simplu pentru găsirea companiilor care au nevoie de soluții ERP/contabilitate.
|
||||
|
||||
## Cum funcționează
|
||||
|
||||
1. **find_leads.py** - Caută companii care angajează contabili/economiști
|
||||
- Folosește Brave Search API
|
||||
- Extrage nume de companii din rezultatele de pe eJobs, BestJobs, etc.
|
||||
- Salvează în CSV pentru review manual
|
||||
|
||||
2. **templates/** - Template-uri email pentru outreach
|
||||
- `template_general.md` - Template generic pentru cold email
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Activează venv
|
||||
cd ~/clawd && source venv/bin/activate
|
||||
|
||||
# Rulează căutarea
|
||||
python tools/lead-gen/find_leads.py --limit 10
|
||||
|
||||
# Output: tools/lead-gen/output/leads.csv
|
||||
```
|
||||
|
||||
## Workflow manual
|
||||
|
||||
1. **Rulează scriptul** (5 min/săptămână)
|
||||
```bash
|
||||
python tools/lead-gen/find_leads.py
|
||||
```
|
||||
|
||||
2. **Review CSV** (5 min)
|
||||
- Deschide `output/leads.csv`
|
||||
- Marchează companiile interesante
|
||||
- Completează CUI, email, website (căutare manuală Google)
|
||||
|
||||
3. **Trimite email** (per companie)
|
||||
- Folosește template din `templates/`
|
||||
- Personalizează cu numele companiei și jobul
|
||||
|
||||
## Status tracking
|
||||
|
||||
În CSV, câmpul `status`:
|
||||
- `new` - proaspăt găsit
|
||||
- `researched` - am completat datele
|
||||
- `contacted` - am trimis email
|
||||
- `replied` - au răspuns
|
||||
- `converted` - client nou! 🎉
|
||||
- `rejected` - nu sunt interesați
|
||||
|
||||
## TODO (îmbunătățiri viitoare)
|
||||
|
||||
- [ ] Auto-enrich cu CUI de pe ANAF
|
||||
- [ ] Validare email (MX lookup)
|
||||
- [ ] Template-uri per industrie
|
||||
- [ ] Integrare cu Gmail pentru trimitere
|
||||
- [ ] Dashboard cu statistici
|
||||
|
||||
## De ce funcționează
|
||||
|
||||
Companiile care **angajează contabili** au nevoie de:
|
||||
- Software de contabilitate
|
||||
- Poate nu sunt mulțumiți de soluția curentă
|
||||
- Sau cresc și au nevoie de ceva mai robust
|
||||
|
||||
Targetarea lor e mult mai eficientă decât cold email random.
|
||||
26
tools/lead-gen/clienti-romfast.csv
Normal file
26
tools/lead-gen/clienti-romfast.csv
Normal file
@@ -0,0 +1,26 @@
|
||||
NUME,RULAJ_ANUAL,COD_FISCAL,RAMURA,NOTE
|
||||
XENOTI S.R.L.,83099.1,RO6743250,HOTEL,"VALENTINA ESTE EXPERT CONTABIL SI DIRECTOR LA XENOTI. SUNTEM CUNOSTINTE VECHI, LA FEL CA SI CU ALEX STEFAN. IN VIRTUTEA RELATIEI VECHI DE PE ALTE FIRME, A VRUT PROGRAMUL ROA SI LA XENOTI. VALENTINA ARE SI ALTE FIRME. FACTUREZ TOTUL PE XENOTI, INCLUSIV PROIECTE DE MIGRARE ALE UNOR FIRME ALE VALENTINEI PE ROA SI DE ACEEA ARE CEA MAI MARE VALOARE. INTRE TIMP AM 3 FACTURI NEACHITATE DE LA XENOTI. NU A AVUT VANZARI BUNE IN ULTIMUL SEZON ESTIVAL"
|
||||
ROMPETROL ENERGY SA,57928.23,RO29923675,ENERGIE TERMICA,"FOSTA UTMIDIA NAVODARI, CLIENT VECHI, CLIENT IDEAL, FACTURI PUTINE, VALOARE MARE, CLIENTI PUTINI DAR MARI, LE-AM FACUT MIGRARE ROA DE LA UTMIDIA NAVODARI. AU SPUS CA TREC LA PROGRAMELE INTERNATIONALE FOLOSITE DE ROMPETROL SA, DAR INCA NU AU TRECUT DE CATIVA ANI"
|
||||
ADMINISTRATIA CANALELOR NAVIGABILE,39907.84,RO11087755,COMPANIE NATIONALA,"ACUM 15 ANI AU AVUT PROGRAME CONTABILE VECHI SI DE FACTURARE SI AU AVUT NEVOIE DE ALTE PROGRAME. CLIENTI VECHI, STABILI. ACUM NECESITA MENTENANTA PUTINA. A FOST MAI GREU SA LE CRESC PRETURILE. SUNT RETICENTI. CLIENT IDEAL"
|
||||
VENDING MASTER SRL,39270,RO33137200,"DISTRIBUTIE CAFEA, AUTOMATE CAFEA","AVIS DATABASE ACCOUNTING ESTE CONTABILUL LUI, ALEX STEFAN, EXPERTUL CONTABIL DE LA AVIS DATABASE ESTE UN FOST COLEG SI PRIN EL AM LUAT FIRMA VENDING CLIENT, DE CAND ERA MAI MICA CA DIMENSIUNE. I-AM CRESCUT TARIFUL IN DECURS DE MAI MULTI ANI PENTRU CA I-A CRESCUT ACTIVITATEA SI SUPORTUL TEHNIC"
|
||||
VADECO SRL,34012.67,RO14707452,SERVICII TRANSPORT,"CLIENT IDEAL, SERVICII, ARE CLIENTI FOARTE PUTINI, DAR DE VALOARE MARE, 4 PERSOANE LA FACTURARE, AGENTI, CONTABIL SI DIRECTOR ECONOMIC CU CARE LUCREZ"
|
||||
EUROPEAN METAL SERVICES SA,32584.67,RO12629765,SERVICII FIER VECHI,"CLIENT IDEAL, SERVICII, ARE CLIENTI FOARTE PUTINI, DAR DE VALOARE MARE, 1 CONTABIL SI DIRECTOR ECONOMIC CU CARE LUCREZ. ARE SI ALTI ANGAJATI BINEINTELES"
|
||||
SOUTH EAST TRUCK SERVICES S.R.L.,31136.42,RO43525632,SERVICE AUTO,"CLIENT IDEAL. CLIENTI PUTINI (POLARIS M HOLDING) DAR MARI. UN RECEPTIONER/GESTIONAR, MECANICI SI PATRONUL CARE ESTE INGINER AUTO"
|
||||
MIDIA GREEN ENERGY SA,19269.2,RO14325363,ENERGIE FOTOVOLTAICA,CLIENT FOARTE VECHI. S-A DESPRINS DIN UTMIDIA NAVODARI CARE A FOST CONVERTITA IN ROMPETROL ENERGY SA. CLIENT IDEAL. LUCREZ DOAR CU DIRECTORUL ECONOMIC CAND ARE NEVOIE DE ASISTENTA LA DECLARATII SAU OPERATII IMOBILIZARI
|
||||
INTREPRINDEREA METALURGICA PENTRU AERONAUTICA META,18102.62,RO16036329,PRODUCTIE,"CLIENT FOARTE VECHI. IDEAL. CA SI EDUARD PUBLISHING A AVUT NEVOI CONTABILE SPORITE PENTRU URMARIREA PRODUCTIEI, COSTURILOR SI ANALIZA CHELTUIELILOR, BUGETELOR"
|
||||
ARGENTA SRL,14482.07,RO3959705,CONSTRUCTII,"CLIENT IDEAL. CLIENTI PUTINI, VALOARE MARE. ARE UN PROGRAM SPECIAL DE URMARIRE PROIECTE/DEVIZE/OFERTE COMPARATIE COSTURI FATA DE OFERTA, PENTRU ANALIZA RANDAMENT LUCRARI CONSTRUCTIE"
|
||||
ETALON DISTRIBUTION S R L,14380,RO42158724,DISTRIBUTIE PRESA,CLIENT VECHI PROVENIT DIN ALT CLIENT - PRIN TRANSFER ACTIVE. VALENTINA ESTE DIRECTOR ECONOMIC SI LA ETALON. AM CONTINUAT ACTIVITATEA CU ETALON PRIN VALENTINA
|
||||
A.B.C. VAL,13487.42,RO3853010,CONSTRUCTII,"A INTRAT IN FALIMENT. NU MAI ESTE CLIENT. CA SI ARGENTA SA, AVEA NEVOI DE URMARIRE PROIECTE, LUCRARI, DEVIZ, OFERTA - AM PROGRAMUL RESPECTIV"
|
||||
EDUARD PUBLISHING,12084.4,RO 25629015,"EDITURA CARTE, PRODUCTIE CARTE","ACUM O DUCE MAI GREU. LA INCEPUTUL RELATIEI, ACUM 15 ANI AVEA ACTIVITATE MAI MARE SI AVEA NEVOI DE URMARIRE PRODUCTIE DE CARTE"
|
||||
AUTOMOTIVE SERVICE SRL,10165,RO18448482,SERVICE AUTO,"CLIENT IDEAL. UN PATRON, UN INGINER, PLUS MECANICI. PLATESTE PROMPT. AM PUTINA ACTIVITATE CU EL CA SUPORT TEHNIC. DE VAZUT CE CIFRA DE AFACERI ARE"
|
||||
CLEVER MOTORS SRL,10165,RO44234984,SERVICE AUTO,"NU PLATESTE LA TIMP, RESTANTE 3-4 LUNI. ESTE UN SERVICE AUTO CARE CRED CA NU ISI PRIMESTE BANII LA TIMP DE LA ASIGURATORI, SAU ARE COMENZI PUTINE. DE VAZUT CE CIFRA DE AFACERI ARE"
|
||||
ALMMA CONTRACTORS GROUP S.R.L.,9223.46,RO37165512,CONSTRUCTII,A DAT FALIMENT. NU MAI ESTE CLIENT. ERA IN GRUPUL A.B.C VAL
|
||||
ROMCONSTRUCT GLASS S.R.L.,8615,RO24498302,PRODUCTIE,"PRODUCTIE GEAMURI, MONTAJ. CLIENT IDEAL DAR IN ULTIMII ANI SI-A REDUS ACTIVITATEA. AVEA NEVOIE DE PROGRAM DE PRODUCTIE, CONSUM, FACTURARE. AM FACUT PROGRAMUL DAR S-A DOVEDIT FOARTE COMPLEX FATA DE CALCULELE LOR DIN EXCEL. FOLOSESC DOAR FACTURAREA DE SERVICII, IN LOC DE PRODUCTIE, CONSUM SI FACTURARE PRODUCTIE. PROBLEMA CEA MAI MARE ERA SI ESTE GESTIUNEA STOCURILOR, DEVIZELE CARE SE FAC IN EXCEL, DAR NU SE VAD IN SOFT. LE LUA PREA MULT TIMP SA FOLOSEASCA PROGRAMELE CONSTRUITE SPECIAL PENTRU PRODUCTIE, DESCARCARE PRODUCTIE SI NU LE-AU FOLOSIT."
|
||||
SIGMA L.C. SERVICE,7274.66,RO21773785,SERVICE AUTO,ARE RESTANTE 3-6 LUNI. E OK CA NIVEL DE CERERI
|
||||
WERT SRL,6000,RO7435479,PRODUCTIE,ESTE CLIENTUL CARAPETRU CONTAB SI FOLOSESTE PROGRAMELE ROA PENTRU CONTABILITATE PRIMARA. NU EMITE FACTURI DIN ROA.
|
||||
DIRECTIA DE PAZA A JUDETULUI CONSTANTA,4314,5639774,SERVICII,A RAMAS DOAR CU PROGRAMUL DE GESTIUNE OBIECTE DE INVENTAR. NU MAI ARE ACTIVITATE
|
||||
DRAFT CONSTRUCT SRL,3600,RO16322932,CONSTRUCTII,A INTRAT IN INSOLVENTA. CLIENT CU PROBLEME FINANCIARE. ESTE CLIENTUL CARAPETRU CONTAB SI FOLOSESTE PROGRAMELE ROA PENTRU CONTABILITATE PRIMARA. NU EMITE FACTURI DIN ROA.
|
||||
DRAFT DINAMIC CONSTRUCT S.R.L.,3600,RO45269115,CONSTRUCTII,A INTRAT IN INSOLVENTA. CLIENT CU PROBLEME FINANCIARE. ESTE CLIENTUL CARAPETRU CONTAB SI FOLOSESTE PROGRAMELE ROA PENTRU CONTABILITATE PRIMARA. NU EMITE FACTURI DIN ROA.
|
||||
BIG TRADE S.A.,2958.38,RO7436261,SERVICII,"NU MAI ESTE CLIENTUL MEU. PLATEA PENTRU UN GRUP DE FIRME MICI, CARE AU TRECUT PE SAGA PENTRU CA NU AVEAU ACTIVITATE"
|
||||
AVIS DATABASE ACCOUNTING S.R.L.,1776.82,40023585,CONTABILITATE,ALEX STEFAN DE LA AVIS DATABASE ACCOUNTING A FOST COLEG CU MINE. SI-A FACUT FIRMA DE CONTABILITATE. VENDING MASTER ESTE CLIENTUL LUI. PRIN ALEX STEFAN L-AM OBTINUT ACUM MULTI ANI.
|
||||
CARAPETRU CONTAB SRL,300,35295575,CONTABILITATE,"SILVIA DE LA CARAPETRU CONTAB ESTE CONTABILA MEA SI FOLOSESTE PROGRAMELE ROA PENTRU FIRMELE EI. WERT, DRAFT DINAMIC, DRAFT CONSTRUCT, SUNT CLIENTII EI"
|
||||
|
229
tools/lead-gen/find_leads.py
Normal file
229
tools/lead-gen/find_leads.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lead Generator Minimal - Găsește companii care au nevoie de soluții ERP/contabilitate.
|
||||
|
||||
Folosește Brave Search API pentru a găsi companii care angajează contabili/economiști.
|
||||
|
||||
Output: leads.csv cu companii pentru review manual
|
||||
|
||||
Usage:
|
||||
python find_leads.py [--limit N]
|
||||
|
||||
Necesită: BRAVE_API_KEY în environment sau ~/.clawdbot/clawdbot.json
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import csv
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
OUTPUT_DIR = Path(__file__).parent / "output"
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
def get_brave_api_key():
|
||||
"""Get Brave API key from clawdbot config."""
|
||||
config_path = Path.home() / ".clawdbot" / "clawdbot.json"
|
||||
if config_path.exists():
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
# Try tools.web.search.apiKey (clawdbot format)
|
||||
api_key = config.get("tools", {}).get("web", {}).get("search", {}).get("apiKey", "")
|
||||
if api_key:
|
||||
return api_key
|
||||
# Fallback to brave.apiKey
|
||||
return config.get("brave", {}).get("apiKey", "")
|
||||
return os.getenv("BRAVE_API_KEY", "")
|
||||
|
||||
def search_brave(query, count=10):
|
||||
"""Search using Brave Search API."""
|
||||
api_key = get_brave_api_key()
|
||||
if not api_key:
|
||||
print("[!] Nu am găsit Brave API key")
|
||||
return []
|
||||
|
||||
url = "https://api.search.brave.com/res/v1/web/search"
|
||||
headers = {
|
||||
"X-Subscription-Token": api_key,
|
||||
"Accept": "application/json"
|
||||
}
|
||||
params = {
|
||||
"q": query,
|
||||
"count": count
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, params=params, timeout=15)
|
||||
data = resp.json()
|
||||
return data.get("web", {}).get("results", [])
|
||||
except Exception as e:
|
||||
print(f"[!] Brave search error: {e}")
|
||||
return []
|
||||
|
||||
def extract_companies_from_results(results):
|
||||
"""Extract company names from search results."""
|
||||
companies = []
|
||||
|
||||
# Patterns for Romanian companies
|
||||
patterns = [
|
||||
r'([A-Z][A-Z\s\-\.&]+(?:S\.R\.L\.|SRL|S\.A\.|SA|S\.C\.))', # COMPANY S.R.L.
|
||||
r'(SC\s+[A-Z][A-Z\s\-\.&]+(?:S\.R\.L\.|SRL|S\.A\.|SA))', # SC COMPANY SRL
|
||||
r'([A-Z][a-zA-Z\s\-\.&]{2,30}(?:S\.R\.L\.|SRL|S\.A\.|SA))', # Mixed case
|
||||
]
|
||||
|
||||
for result in results:
|
||||
text = f"{result.get('title', '')} {result.get('description', '')}"
|
||||
|
||||
for pattern in patterns:
|
||||
matches = re.findall(pattern, text)
|
||||
for match in matches:
|
||||
company = match.strip()
|
||||
# Clean up
|
||||
company = re.sub(r'\s+', ' ', company)
|
||||
if len(company) > 5 and len(company) < 80:
|
||||
companies.append({
|
||||
"company": company,
|
||||
"source_url": result.get("url", ""),
|
||||
"context": text[:200]
|
||||
})
|
||||
|
||||
return companies
|
||||
|
||||
def clean_company_name(name):
|
||||
"""Clean company name from prefix garbage."""
|
||||
# Remove common prefixes that get caught by regex
|
||||
prefixes_to_remove = [
|
||||
r'^(?:Senior|Junior|Contabil|Economist|Director\s+Economic|Expert|Specialist)\s+',
|
||||
r'^(?:RON|EUR|USD)\s+',
|
||||
r'^(?:Bucuresti|Cluj|Iasi|Brasov|Constanta)\s+',
|
||||
r'^\d+[\s\-]+',
|
||||
]
|
||||
|
||||
result = name.strip()
|
||||
for pattern in prefixes_to_remove:
|
||||
result = re.sub(pattern, '', result, flags=re.IGNORECASE)
|
||||
|
||||
# Clean trailing garbage
|
||||
result = re.sub(r'\s*-\s*$', '', result)
|
||||
result = re.sub(r'\s+', ' ', result).strip()
|
||||
|
||||
return result
|
||||
|
||||
def deduplicate(leads):
|
||||
"""Elimină duplicate după numele companiei."""
|
||||
seen = set()
|
||||
unique = []
|
||||
for lead in leads:
|
||||
# Clean company name
|
||||
lead["company"] = clean_company_name(lead["company"])
|
||||
|
||||
# Normalize for comparison
|
||||
company_norm = re.sub(r'[^a-z0-9]', '', lead["company"].lower())
|
||||
|
||||
# Skip too short or invalid
|
||||
if len(company_norm) < 5:
|
||||
continue
|
||||
|
||||
# Skip obvious non-companies
|
||||
skip_patterns = [
|
||||
r'^emea\s',
|
||||
r'^staff\s',
|
||||
r'accountant',
|
||||
r'^bestjobs',
|
||||
r'^ejobs',
|
||||
r'^hipo',
|
||||
]
|
||||
if any(re.search(p, lead["company"], re.IGNORECASE) for p in skip_patterns):
|
||||
continue
|
||||
|
||||
if company_norm not in seen:
|
||||
seen.add(company_norm)
|
||||
unique.append(lead)
|
||||
return unique
|
||||
|
||||
def enrich_leads(leads):
|
||||
"""Adaugă câmpuri pentru tracking."""
|
||||
for lead in leads:
|
||||
lead["found_date"] = datetime.now().isoformat()[:10]
|
||||
lead["cui"] = ""
|
||||
lead["email"] = ""
|
||||
lead["website"] = ""
|
||||
lead["phone"] = ""
|
||||
lead["status"] = "new" # new, researched, contacted, replied, converted, rejected
|
||||
lead["notes"] = ""
|
||||
lead["industry"] = ""
|
||||
return leads
|
||||
|
||||
def save_leads(leads, filename="leads.csv"):
|
||||
"""Salvează leads în CSV pentru review."""
|
||||
output_file = OUTPUT_DIR / filename
|
||||
|
||||
fieldnames = ["company", "industry", "source_url", "found_date",
|
||||
"cui", "email", "website", "phone", "status", "notes"]
|
||||
|
||||
# Remove context from output (used only for extraction)
|
||||
for lead in leads:
|
||||
lead.pop("context", None)
|
||||
|
||||
with open(output_file, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
|
||||
writer.writeheader()
|
||||
writer.writerows(leads)
|
||||
|
||||
return output_file
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Lead Generator Minimal")
|
||||
parser.add_argument("--limit", type=int, default=10, help="Results per search query")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🔍 Căutare leads via Brave Search...")
|
||||
|
||||
# Search queries - companii care angajează contabili
|
||||
queries = [
|
||||
'site:ejobs.ro contabil angajare 2026',
|
||||
'site:ejobs.ro economist angajare',
|
||||
'site:bestjobs.eu contabil Romania',
|
||||
'site:hipo.ro contabil angajare',
|
||||
'"angajam contabil" Romania firma',
|
||||
'"cautam economist" Romania SRL',
|
||||
]
|
||||
|
||||
all_leads = []
|
||||
|
||||
for query in queries:
|
||||
print(f" → {query[:50]}...")
|
||||
results = search_brave(query, count=args.limit)
|
||||
companies = extract_companies_from_results(results)
|
||||
all_leads.extend(companies)
|
||||
print(f" Găsite: {len(companies)} companii")
|
||||
|
||||
# Deduplică
|
||||
unique_leads = deduplicate(all_leads)
|
||||
print(f"\n📊 Total: {len(all_leads)} → {len(unique_leads)} unice")
|
||||
|
||||
# Îmbogățește
|
||||
enriched = enrich_leads(unique_leads)
|
||||
|
||||
# Salvează
|
||||
output_file = save_leads(enriched)
|
||||
print(f"\n✅ Salvat: {output_file}")
|
||||
|
||||
# Afișează lista
|
||||
print(f"\n📋 {len(enriched)} companii găsite:")
|
||||
for i, lead in enumerate(enriched, 1):
|
||||
print(f" {i}. {lead['company']}")
|
||||
|
||||
print(f"\n💡 Următorii pași:")
|
||||
print(f" 1. Deschide {output_file}")
|
||||
print(f" 2. Completează CUI, email, website pentru cele interesante")
|
||||
print(f" 3. Marchează status: researched → contacted → replied")
|
||||
|
||||
return enriched
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
20
tools/lead-gen/output/leads.csv
Normal file
20
tools/lead-gen/output/leads.csv
Normal file
@@ -0,0 +1,20 @@
|
||||
company,industry,source_url,found_date,cui,email,website,phone,status,notes
|
||||
TAXASIST ADVICES S.R.L.,,https://www.ejobs.ro/en/jobs/contabil,2026-02-04,,,,,new,
|
||||
FINANCE CONSULTING S.R.L.,,https://www.ejobs.ro/en/jobs/contabil-junior,2026-02-04,,,,,new,
|
||||
CABINET EXPERT CONTABIL DOCHIAN SI ASOCIATII SRL,,https://www.ejobs.ro/en/jobs/contabil-junior,2026-02-04,,,,,new,
|
||||
TPA Transilvania Contax S.R.L.,,https://www.ejobs.ro/en/jobs/contabil-junior,2026-02-04,,,,,new,
|
||||
DOCHIAN SI ASOCIATII SRL,,https://www.ejobs.ro/en/jobs/contabil-junior,2026-02-04,,,,,new,
|
||||
SC COMGABY MOLN SRL,,https://www.ejobs.ro/en/jobs/cluj-napoca/contabil-junior,2026-02-04,,,,,new,
|
||||
MOL ROMANIA Petroleum Products SRL,,https://www.ejobs.ro/en/jobs/cluj-napoca/contabil-junior,2026-02-04,,,,,new,
|
||||
TRAIN TO PERFORM SRL,,https://www.ejobs.ro/en/jobs/iasi/contabil,2026-02-04,,,,,new,
|
||||
SC PANIFCOM SRL,,https://www.ejobs.ro/en/jobs/iasi/contabil,2026-02-04,,,,,new,
|
||||
SWISS MANAGEMENT COMPANY SRL,,https://www.ejobs.ro/en/jobs/brasov/contabil,2026-02-04,,,,,new,
|
||||
SC CROCO SRL,,https://www.ejobs.ro/en/jobs/brasov/contabil,2026-02-04,,,,,new,
|
||||
OV UNITY PLACE S.R.L.,,https://www.ejobs.ro/en/jobs/brasov/contabil,2026-02-04,,,,,new,
|
||||
EREN CONS SRL,,https://www.ejobs.ro/en/jobs/brasov/contabil,2026-02-04,,,,,new,
|
||||
ADECCO RESURSE UMANE SRL,,https://www.ejobs.ro/en/jobs/financiar-contabilitate/no-experience,2026-02-04,,,,,new,
|
||||
Global Services Bucharest SRL,,https://www.ejobs.ro/en/jobs/financiar-contabilitate/no-experience,2026-02-04,,,,,new,
|
||||
TRAVEL BRANDS S.A.,,https://www.ejobs.ro/en/jobs/bucuresti/contabil,2026-02-04,,,,,new,
|
||||
B BUSINESS GOLD S.R.L.,,https://www.ejobs.ro/en/jobs/financiar-contabilitate,2026-02-04,,,,,new,
|
||||
Contab SRL,,https://www.ejobs.ro/en/jobs/financiar-contabilitate,2026-02-04,,,,,new,
|
||||
MASPEX ROMANIA SRL,,https://www.bestjobs.eu/en/jobs/contabil+fara+experienta,2026-02-04,,,,,new,
|
||||
|
30
tools/lead-gen/templates/template_general.md
Normal file
30
tools/lead-gen/templates/template_general.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Template Email - General
|
||||
|
||||
**Subiect:** Soluție software contabilitate - {company}
|
||||
|
||||
---
|
||||
|
||||
Bună ziua,
|
||||
|
||||
Am văzut că {company} caută {job_title}.
|
||||
|
||||
De peste 25 de ani dezvoltăm **ROA** - un sistem ERP complet pentru contabilitate și gestiune, folosit de companii din România care au nevoie de:
|
||||
|
||||
- ✅ Contabilitate completă (generală + analitică)
|
||||
- ✅ Gestiune stocuri și facturare
|
||||
- ✅ Rapoarte ANAF automate (D300, D394, D390, e-Factura)
|
||||
- ✅ Interfață web pentru acces de oriunde
|
||||
|
||||
**De ce ROA:**
|
||||
- Soluție românească, adaptată legislației locale
|
||||
- Bază de date Oracle - performanță și securitate
|
||||
- Suport tehnic direct de la dezvoltator
|
||||
- Prețuri competitive vs. soluțiile multinaționale
|
||||
|
||||
Dacă vă interesează o demonstrație gratuită de 30 minute, răspundeți la acest email sau sunați la {phone}.
|
||||
|
||||
Cu respect,
|
||||
{sender_name}
|
||||
{sender_company}
|
||||
{sender_phone}
|
||||
{sender_email}
|
||||
155
tools/pauza_random.py
Normal file
155
tools/pauza_random.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Alege o tehnică de pauză random din memory/tehnici-pauza.md
|
||||
În funcție de ora curentă (București = UTC+2):
|
||||
- 09:00-17:00 → BIROU
|
||||
- 18:00+ → ACASĂ
|
||||
|
||||
Output include: tehnica + rezultat + sursă (pentru context)
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Timezone București (UTC+2, simplificat)
|
||||
TZ_OFFSET = timedelta(hours=2)
|
||||
|
||||
def get_bucharest_hour():
|
||||
"""Returnează ora curentă în București."""
|
||||
utc_now = datetime.now(timezone.utc)
|
||||
bucharest_now = utc_now + TZ_OFFSET
|
||||
return bucharest_now.hour
|
||||
|
||||
def parse_tehnici(filepath):
|
||||
"""Parsează fișierul și returnează dict cu BIROU și ACASA."""
|
||||
content = Path(filepath).read_text(encoding='utf-8')
|
||||
|
||||
tehnici = {'BIROU': [], 'ACASA': []}
|
||||
current_section = None
|
||||
current_tehnica = None
|
||||
current_detalii = []
|
||||
current_rezultat = None
|
||||
current_sursa = None
|
||||
|
||||
for line in content.split('\n'):
|
||||
# Detectează secțiunea
|
||||
if '## BIROU' in line:
|
||||
current_section = 'BIROU'
|
||||
continue
|
||||
elif '## ACASĂ' in line or '## ACASA' in line:
|
||||
current_section = 'ACASA'
|
||||
continue
|
||||
elif line.startswith('## ') or line.startswith('---'):
|
||||
# Altă secțiune (Surse, etc.) - salvează și oprește
|
||||
if current_section and current_tehnica:
|
||||
tehnici[current_section].append({
|
||||
'titlu': current_tehnica,
|
||||
'detalii': current_detalii,
|
||||
'rezultat': current_rezultat,
|
||||
'sursa': current_sursa
|
||||
})
|
||||
current_section = None
|
||||
continue
|
||||
|
||||
if not current_section:
|
||||
continue
|
||||
|
||||
# Detectează titlu tehnică (### Titlu)
|
||||
if line.startswith('### '):
|
||||
# Salvează tehnica anterioară
|
||||
if current_tehnica:
|
||||
tehnici[current_section].append({
|
||||
'titlu': current_tehnica,
|
||||
'detalii': current_detalii,
|
||||
'rezultat': current_rezultat,
|
||||
'sursa': current_sursa
|
||||
})
|
||||
current_tehnica = line[4:].strip()
|
||||
current_detalii = []
|
||||
current_rezultat = None
|
||||
current_sursa = None
|
||||
elif line.startswith('- **Rezultat:**'):
|
||||
current_rezultat = line.replace('- **Rezultat:**', '').strip()
|
||||
elif line.startswith('- **Sursă:**'):
|
||||
current_sursa = line.replace('- **Sursă:**', '').strip()
|
||||
elif line.startswith('- ') and current_tehnica:
|
||||
# Detaliu (bullet point, exclude doar Rezultat și Sursă)
|
||||
detaliu = line[2:].strip()
|
||||
# Curăță bold markers
|
||||
detaliu = re.sub(r'\*\*([^*]+)\*\*:', r'\1:', detaliu)
|
||||
if detaliu:
|
||||
current_detalii.append(detaliu)
|
||||
|
||||
# Salvează ultima tehnică
|
||||
if current_section and current_tehnica:
|
||||
tehnici[current_section].append({
|
||||
'titlu': current_tehnica,
|
||||
'detalii': current_detalii,
|
||||
'rezultat': current_rezultat,
|
||||
'sursa': current_sursa
|
||||
})
|
||||
|
||||
return tehnici
|
||||
|
||||
def formateaza_mesaj(tehnica):
|
||||
"""Formatează tehnica pentru mesaj Discord cu context."""
|
||||
titlu = tehnica['titlu']
|
||||
detalii = tehnica['detalii']
|
||||
rezultat = tehnica.get('rezultat')
|
||||
sursa = tehnica.get('sursa')
|
||||
|
||||
# Alege un detaliu random dacă sunt mai multe
|
||||
if detalii:
|
||||
detaliu = random.choice(detalii)
|
||||
else:
|
||||
detaliu = ""
|
||||
|
||||
# Construiește mesajul
|
||||
lines = [f"**{titlu}**"]
|
||||
if detaliu:
|
||||
lines.append(detaliu)
|
||||
if rezultat:
|
||||
lines.append(f"📊 _{rezultat}_")
|
||||
if sursa:
|
||||
# Extrage link-uri din markdown [text](url) și afișează separat
|
||||
link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
|
||||
links = link_pattern.findall(sursa)
|
||||
# Curăță sursa de markdown links pentru text simplu
|
||||
sursa_clean = link_pattern.sub(r'\1', sursa)
|
||||
lines.append(f"📚 {sursa_clean}")
|
||||
# Adaugă link-urile pe linii separate
|
||||
for text, url in links:
|
||||
lines.append(f"🔗 <{url}>")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def main():
|
||||
filepath = Path(__file__).parent.parent / 'memory/kb/tehnici-pauza.md'
|
||||
|
||||
if not filepath.exists():
|
||||
print("Fișierul tehnici-pauza.md nu există!")
|
||||
return
|
||||
|
||||
hora = get_bucharest_hour()
|
||||
tehnici = parse_tehnici(filepath)
|
||||
|
||||
# Alege secțiunea în funcție de oră
|
||||
if 9 <= hora <= 17:
|
||||
sectiune = 'BIROU'
|
||||
else:
|
||||
sectiune = 'ACASA'
|
||||
|
||||
if not tehnici[sectiune]:
|
||||
print(f"Nu am tehnici pentru secțiunea {sectiune}!")
|
||||
return
|
||||
|
||||
# Alege o tehnică random
|
||||
tehnica = random.choice(tehnici[sectiune])
|
||||
mesaj = formateaza_mesaj(tehnica)
|
||||
|
||||
print(mesaj)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
595
tools/process_bon.py
Normal file
595
tools/process_bon.py
Normal file
@@ -0,0 +1,595 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Procesare bon fiscal: PDF → OCR API → SQLite API → Oracle
|
||||
|
||||
Usage:
|
||||
python process_bon.py <pdf_path> [--save]
|
||||
|
||||
--save Salvează efectiv în Oracle (altfel dry run)
|
||||
|
||||
Fluxul:
|
||||
1. OCR extract via API (http://10.0.20.171:8000/api/data-entry/ocr/extract)
|
||||
2. Save receipt via API (http://10.0.20.171:8000/api/data-entry/receipts/) - TOATE datele
|
||||
3. Save to Oracle:
|
||||
- Verifică/creează partener
|
||||
- Verifică TVA la încasare (CALENDAR.TVA_INCASARE)
|
||||
- Generează note contabile corecte
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import requests
|
||||
import oracledb
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load .env from parent directory
|
||||
load_dotenv(Path(__file__).parent.parent / ".env")
|
||||
|
||||
# === CONFIG ===
|
||||
API_BASE = "http://10.0.20.171:8000"
|
||||
API_USER = "MARIUS M"
|
||||
API_PASS = os.getenv("ROA_API_PASSWORD", "")
|
||||
SERVER_ID = "central"
|
||||
COMPANY_ID = 110 # MARIUSM AUTO
|
||||
|
||||
ORACLE_CONFIG = {
|
||||
"user": "MARIUSM_AUTO",
|
||||
"password": os.getenv("ORACLE_PASSWORD", ""),
|
||||
"dsn": "10.0.20.121:1521/ROA"
|
||||
}
|
||||
|
||||
# Mapare CUI → cont cheltuială
|
||||
CUI_TO_CONT = {
|
||||
"11201891": "6022", # MOL
|
||||
"1590082": "6022", # OMV Petrom
|
||||
"14991381": "6022", # Rompetrol
|
||||
"10562600": "6021", # Dedeman / Five Holding (Brick)
|
||||
"1879865": "6021", # Five Holding
|
||||
}
|
||||
|
||||
# Mapare cotă TVA → (ID_JTVA baza, ID_JTVA tva, TAXCODE, TAXCODE_TVAI)
|
||||
# Pentru achiziții interne neexigibile (TVA la încasare)
|
||||
JTVA_NEEX = {
|
||||
21: (210, 211, 301104, 301305), # ACH. INT. NEEX. 21%
|
||||
19: (188, 189, 301101, 301301), # ACH. INT. NEEX. 19%
|
||||
11: (214, 215, 301105, 301306), # ACH. INT. NEEX. 11%
|
||||
9: (172, 173, 301102, 301302), # ACH. INT. NEEX. 9%
|
||||
5: (174, 175, 301103, 301303), # ACH. INT. NEEX. 5%
|
||||
}
|
||||
|
||||
# Pentru achiziții interne normale (fără TVA la încasare)
|
||||
JTVA_NORMAL = {
|
||||
21: (208, 209, 301104, 301305), # ACH. INT. 21%
|
||||
19: (None, None, 301101, 301301),
|
||||
9: (None, None, 301102, 301302),
|
||||
}
|
||||
|
||||
|
||||
def get_cont(cui: str) -> str:
|
||||
"""Mapare CUI → cont cheltuială."""
|
||||
cui_clean = (cui or "").upper().replace("RO", "").strip()
|
||||
return CUI_TO_CONT.get(cui_clean, "6028") # 6028 = alte cheltuieli
|
||||
|
||||
|
||||
class APIClient:
|
||||
"""Client pentru roa2web API."""
|
||||
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.token = None
|
||||
self.session = requests.Session()
|
||||
|
||||
def login(self, username: str, password: str, server_id: str) -> bool:
|
||||
"""Login și obține token."""
|
||||
r = self.session.post(
|
||||
f"{self.base_url}/api/auth/login",
|
||||
json={"username": username, "password": password, "server_id": server_id}
|
||||
)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
self.token = data.get("access_token")
|
||||
self.session.headers["Authorization"] = f"Bearer {self.token}"
|
||||
return True
|
||||
print(f"Login failed: {r.status_code} - {r.text}")
|
||||
return False
|
||||
|
||||
def ocr_extract(self, file_path: Path) -> dict:
|
||||
"""Submit OCR job și așteaptă rezultatul."""
|
||||
# Determine mime type
|
||||
suffix = file_path.suffix.lower()
|
||||
if suffix == ".pdf":
|
||||
mime_type = "application/pdf"
|
||||
elif suffix in (".jpg", ".jpeg"):
|
||||
mime_type = "image/jpeg"
|
||||
elif suffix == ".png":
|
||||
mime_type = "image/png"
|
||||
else:
|
||||
# Try to detect from content
|
||||
with open(file_path, "rb") as f:
|
||||
header = f.read(8)
|
||||
if header[:4] == b'%PDF':
|
||||
mime_type = "application/pdf"
|
||||
suffix = ".pdf"
|
||||
elif header[:3] == b'\xff\xd8\xff':
|
||||
mime_type = "image/jpeg"
|
||||
suffix = ".jpg"
|
||||
elif header[:8] == b'\x89PNG\r\n\x1a\n':
|
||||
mime_type = "image/png"
|
||||
suffix = ".png"
|
||||
else:
|
||||
mime_type = "application/pdf" # default
|
||||
suffix = ".pdf"
|
||||
|
||||
# Use proper filename with extension
|
||||
filename = file_path.stem + suffix if not file_path.suffix else file_path.name
|
||||
|
||||
# Submit
|
||||
with open(file_path, "rb") as f:
|
||||
r = self.session.post(
|
||||
f"{self.base_url}/api/data-entry/ocr/extract",
|
||||
files={"file": (filename, f, mime_type)}
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return {"success": False, "error": f"OCR submit failed: {r.text}"}
|
||||
|
||||
job_id = r.json().get("job_id")
|
||||
print(f" OCR job: {job_id}")
|
||||
|
||||
# Wait for result (max 60s per request, retry if pending)
|
||||
for _ in range(4): # Max 4 retries = ~240s total
|
||||
r = self.session.get(
|
||||
f"{self.base_url}/api/data-entry/ocr/jobs/{job_id}/wait",
|
||||
params={"timeout": 60, "wait_for_terminal": "true"},
|
||||
timeout=70
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return {"success": False, "error": f"OCR wait failed: {r.text}"}
|
||||
|
||||
data = r.json()
|
||||
status = data.get("status")
|
||||
|
||||
if status == "completed":
|
||||
return {"success": True, "result": data.get("result"), "time_ms": data.get("processing_time_ms")}
|
||||
elif status == "failed":
|
||||
return {"success": False, "error": data.get("error") or "OCR failed"}
|
||||
# Still pending/processing - will retry
|
||||
|
||||
return {"success": False, "error": "OCR timeout"}
|
||||
|
||||
def create_receipt(self, ocr_result: dict, company_id: int) -> dict:
|
||||
"""Creează receipt în SQLite via API cu TOATE datele."""
|
||||
# Parse date
|
||||
date_str = ocr_result.get("receipt_date")
|
||||
if date_str:
|
||||
receipt_date = date_str[:10] # YYYY-MM-DD
|
||||
else:
|
||||
receipt_date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Build TVA breakdown from OCR
|
||||
tva_breakdown = []
|
||||
for tva_entry in (ocr_result.get("tva_entries") or []):
|
||||
tva_breakdown.append({
|
||||
"code": tva_entry.get("code"),
|
||||
"percent": tva_entry.get("percent"),
|
||||
"amount": float(tva_entry.get("amount") or 0)
|
||||
})
|
||||
|
||||
# Build payment methods from OCR
|
||||
payment_methods = []
|
||||
for pm in (ocr_result.get("payment_methods") or []):
|
||||
payment_methods.append({
|
||||
"method": pm.get("method"),
|
||||
"amount": float(pm.get("amount") or 0)
|
||||
})
|
||||
|
||||
# Determine payment mode
|
||||
payment_mode = ocr_result.get("suggested_payment_mode") or "casa"
|
||||
# If has CARD payment, it's "banca"
|
||||
if any(pm.get("method", "").upper() == "CARD" for pm in payment_methods):
|
||||
payment_mode = "banca"
|
||||
elif any(pm.get("method", "").upper() == "NUMERAR" for pm in payment_methods):
|
||||
payment_mode = "casa"
|
||||
|
||||
payload = {
|
||||
"receipt_type": "bon_fiscal",
|
||||
"direction": "cheltuiala",
|
||||
"receipt_number": ocr_result.get("receipt_number"),
|
||||
"receipt_series": ocr_result.get("receipt_series"),
|
||||
"receipt_date": receipt_date,
|
||||
"amount": float(ocr_result.get("amount") or 0),
|
||||
"partner_name": ocr_result.get("partner_name"),
|
||||
"cui": ocr_result.get("cui"),
|
||||
"tva_total": float(ocr_result.get("tva_total") or 0),
|
||||
"tva_breakdown": tva_breakdown if tva_breakdown else None,
|
||||
"payment_methods": payment_methods if payment_methods else None,
|
||||
"payment_mode": payment_mode,
|
||||
"company_id": company_id,
|
||||
"vendor_address": ocr_result.get("address"),
|
||||
"items_count": ocr_result.get("items_count"),
|
||||
"ocr_raw_text": ocr_result.get("raw_text"),
|
||||
}
|
||||
|
||||
# Remove None values
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
self.session.headers["X-Selected-Company"] = str(company_id)
|
||||
r = self.session.post(
|
||||
f"{self.base_url}/api/data-entry/receipts/",
|
||||
json=payload
|
||||
)
|
||||
|
||||
if r.status_code in (200, 201):
|
||||
return {"success": True, "receipt": r.json()}
|
||||
else:
|
||||
return {"success": False, "error": f"Create receipt failed: {r.text}"}
|
||||
|
||||
|
||||
def get_or_create_partner(cursor, cui: str, name: str, address: str = None) -> int:
|
||||
"""Găsește sau creează partener în Oracle. Returnează ID_PART."""
|
||||
cui_clean = (cui or "").upper().replace("RO", "").strip()
|
||||
|
||||
if not cui_clean:
|
||||
return 0 # No CUI, no partner
|
||||
|
||||
# Try to find existing partner
|
||||
cursor.execute("""
|
||||
SELECT ID_PART FROM NOM_PARTENERI
|
||||
WHERE COD_FISCAL = :cui OR COD_FISCAL = :cui2
|
||||
""", cui=cui_clean, cui2="RO" + cui_clean)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return row[0] # Found existing partner
|
||||
|
||||
# Create new partner
|
||||
cursor.execute("SELECT SEQ_NOM_PARTENERI.NEXTVAL FROM DUAL")
|
||||
new_id = cursor.fetchone()[0]
|
||||
|
||||
# Clean name
|
||||
partner_name = (name or f"PARTENER {cui_clean}")[:100]
|
||||
partner_address = (address or "")[:200]
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO NOM_PARTENERI (ID_PART, NUME, COD_FISCAL, ADRESA, STERS, INACTIV)
|
||||
VALUES (:id_part, :nume, :cui, :adresa, 0, 0)
|
||||
""", id_part=new_id, nume=partner_name, cui=cui_clean, adresa=partner_address)
|
||||
|
||||
print(f" ➕ Partener nou creat: ID={new_id}, CUI={cui_clean}, Nume={partner_name}")
|
||||
return new_id
|
||||
|
||||
|
||||
def check_tva_incasare(cursor, an: int, luna: int) -> bool:
|
||||
"""Verifică dacă firma e plătitoare de TVA la încasare în perioada dată."""
|
||||
cursor.execute("""
|
||||
SELECT NVL(TVA_INCASARE, 0) FROM CALENDAR
|
||||
WHERE AN = :an AND LUNA = :luna
|
||||
""", an=an, luna=luna)
|
||||
row = cursor.fetchone()
|
||||
return row[0] == 1 if row else False
|
||||
|
||||
|
||||
def save_to_oracle(ocr_result: dict, do_commit: bool = False) -> dict:
|
||||
"""Salvează nota contabilă în Oracle cu toate regulile."""
|
||||
conn = oracledb.connect(**ORACLE_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Parse date
|
||||
date_str = ocr_result.get("receipt_date")
|
||||
if date_str:
|
||||
receipt_date = datetime.strptime(date_str[:10], "%Y-%m-%d").date()
|
||||
else:
|
||||
receipt_date = datetime.now().date()
|
||||
|
||||
an, luna = receipt_date.year, receipt_date.month
|
||||
|
||||
# 1. Get or create partner
|
||||
id_part = get_or_create_partner(
|
||||
cursor,
|
||||
ocr_result.get("cui"),
|
||||
ocr_result.get("partner_name"),
|
||||
ocr_result.get("address")
|
||||
)
|
||||
print(f" Partner ID: {id_part}")
|
||||
|
||||
# 2. Check TVA la încasare
|
||||
tva_incasare = check_tva_incasare(cursor, an, luna)
|
||||
cont_tva = "4428" if tva_incasare else "4426"
|
||||
print(f" TVA la încasare: {'DA (4428)' if tva_incasare else 'NU (4426)'}")
|
||||
|
||||
# 3. Determine payment type
|
||||
payment_methods = ocr_result.get("payment_methods") or []
|
||||
has_cash = any(pm.get("method", "").upper() == "NUMERAR" for pm in payment_methods)
|
||||
has_card = any(pm.get("method", "").upper() == "CARD" for pm in payment_methods)
|
||||
|
||||
# If no payment info, assume cash
|
||||
if not payment_methods:
|
||||
has_cash = True
|
||||
|
||||
print(f" Plată: {'NUMERAR' if has_cash else ''}{' + ' if has_cash and has_card else ''}{'CARD' if has_card else ''}")
|
||||
|
||||
# 4. Init PACK_CONTAFIN
|
||||
cursor.callproc('PACK_CONTAFIN.INITIALIZEAZA_SCRIERE_ACT_RUL',
|
||||
[0, datetime.now(), an, luna, 0, 0, 0, 0])
|
||||
|
||||
# 5. Get next COD
|
||||
cursor.execute(
|
||||
"SELECT NVL(MAX(COD), 0) + 1 FROM ACT WHERE AN = :an AND LUNA = :luna",
|
||||
an=an, luna=luna
|
||||
)
|
||||
cod = cursor.fetchone()[0]
|
||||
|
||||
# 6. Calculate amounts
|
||||
total = float(ocr_result.get("amount") or 0)
|
||||
tva = float(ocr_result.get("tva_total") or 0)
|
||||
fara_tva = total - tva
|
||||
|
||||
nract = ocr_result.get("receipt_number", "")
|
||||
nract = int(nract) if str(nract).isdigit() else 0
|
||||
|
||||
cont_cheltuiala = get_cont(ocr_result.get("cui") or "")
|
||||
expl = f"OCR: {ocr_result.get('partner_name') or 'N/A'}"[:100]
|
||||
|
||||
print(f" COD: {cod}, Cont: {cont_cheltuiala}")
|
||||
print(f" Total: {total}, Bază: {fara_tva}, TVA: {tva}")
|
||||
|
||||
# 7. Process TVA entries from OCR (pot fi mai multe cote TVA)
|
||||
tva_entries = ocr_result.get("tva_entries") or []
|
||||
|
||||
# 8. Build accounting lines
|
||||
lines = []
|
||||
|
||||
# Calculate base for each TVA rate
|
||||
if tva_entries:
|
||||
# Process each TVA entry separately
|
||||
for tva_entry in tva_entries:
|
||||
tva_rate = tva_entry.get("percent") or 21
|
||||
tva_amount = float(tva_entry.get("amount") or 0)
|
||||
|
||||
if tva_amount <= 0:
|
||||
continue
|
||||
|
||||
# Calculate base for this TVA rate
|
||||
base_amount = tva_amount / (tva_rate / 100)
|
||||
|
||||
# Get ID_JTVA_COLOANA and TAXCODE based on TVA rate and TVA la încasare
|
||||
if tva_incasare:
|
||||
jtva_data = JTVA_NEEX.get(tva_rate, (210, 211, 301104, 301305))
|
||||
else:
|
||||
jtva_data = JTVA_NORMAL.get(tva_rate, (208, 209, 301104, 301305))
|
||||
|
||||
jtva_baza, jtva_tva, taxcode_normal, taxcode_tvai = jtva_data
|
||||
taxcode = taxcode_tvai if tva_incasare else taxcode_normal
|
||||
|
||||
print(f" TVA {tva_rate}%: baza={base_amount:.2f}, tva={tva_amount:.2f}, JTVA=({jtva_baza},{jtva_tva}), TAXCODE={taxcode}")
|
||||
|
||||
# Linia cheltuială pentru această cotă
|
||||
lines.append({
|
||||
"scd": cont_cheltuiala, "scc": "401",
|
||||
"suma": base_amount, "expl": expl,
|
||||
"id_partc": id_part, "id_partd": 0,
|
||||
"id_jtva": jtva_baza,
|
||||
"taxcode": taxcode
|
||||
})
|
||||
|
||||
# Linia TVA pentru această cotă
|
||||
proc_tva = 1 + tva_rate / 100 # 1.21, 1.19, etc.
|
||||
lines.append({
|
||||
"scd": cont_tva, "scc": "401",
|
||||
"suma": tva_amount, "expl": f"TVA {tva_rate}% {expl}"[:100],
|
||||
"id_partc": id_part, "id_partd": 0,
|
||||
"proc_tva": proc_tva,
|
||||
"id_jtva": jtva_tva,
|
||||
"taxcode": taxcode
|
||||
})
|
||||
else:
|
||||
# Fallback: use total amounts if no tva_entries
|
||||
if fara_tva > 0:
|
||||
tva_rate = round(tva / fara_tva * 100) if fara_tva > 0 else 21
|
||||
else:
|
||||
tva_rate = 21
|
||||
|
||||
if tva_incasare:
|
||||
jtva_data = JTVA_NEEX.get(tva_rate, (210, 211, 301104, 301305))
|
||||
else:
|
||||
jtva_data = JTVA_NORMAL.get(tva_rate, (208, 209, 301104, 301305))
|
||||
|
||||
jtva_baza, jtva_tva, taxcode_normal, taxcode_tvai = jtva_data
|
||||
taxcode = taxcode_tvai if tva_incasare else taxcode_normal
|
||||
|
||||
print(f" TVA {tva_rate}% (estimat): JTVA=({jtva_baza},{jtva_tva}), TAXCODE={taxcode}")
|
||||
|
||||
lines.append({
|
||||
"scd": cont_cheltuiala, "scc": "401",
|
||||
"suma": fara_tva, "expl": expl,
|
||||
"id_partc": id_part, "id_partd": 0,
|
||||
"id_jtva": jtva_baza,
|
||||
"taxcode": taxcode
|
||||
})
|
||||
|
||||
if tva > 0:
|
||||
proc_tva = 1 + tva_rate / 100
|
||||
lines.append({
|
||||
"scd": cont_tva, "scc": "401",
|
||||
"suma": tva, "expl": f"TVA {tva_rate}% {expl}"[:100],
|
||||
"id_partc": id_part, "id_partd": 0,
|
||||
"proc_tva": proc_tva,
|
||||
"id_jtva": jtva_tva,
|
||||
"taxcode": taxcode
|
||||
})
|
||||
|
||||
# Linia plată din casă (DOAR dacă plată numerar)
|
||||
if has_cash and not has_card:
|
||||
lines.append({
|
||||
"scd": "401", "scc": "5311",
|
||||
"suma": total, "expl": f"Plata {expl}"[:100],
|
||||
"id_partc": 0, "id_partd": id_part,
|
||||
"id_jtva": None, # Nu are JTVA pentru plată
|
||||
"taxcode": None
|
||||
})
|
||||
# Dacă plată CARD - nu se face nota 401=5311 (se face la extras bancar)
|
||||
|
||||
# ID_FDOC = 17 pentru BON FISCAL
|
||||
id_fdoc = 17
|
||||
|
||||
# 9. Insert lines
|
||||
for line in lines:
|
||||
proc_tva = line.get("proc_tva") or 0 # Default 0 for non-TVA lines
|
||||
id_jtva = line.get("id_jtva") # Poate fi None pentru plăți
|
||||
taxcode = line.get("taxcode") # Poate fi None pentru plăți
|
||||
cursor.execute("""
|
||||
INSERT INTO ACT_TEMP (
|
||||
LUNA, AN, COD, DATAIREG, DATAACT, NRACT,
|
||||
EXPLICATIA, SCD, SCC, SUMA, PROC_TVA,
|
||||
ID_PARTC, ID_PARTD, ID_FDOC, ID_JTVA_COLOANA, TAXCODE, ID_UTIL, DATAORA,
|
||||
STERS, ID_SET, ID_SUCURSALA
|
||||
) VALUES (
|
||||
:luna, :an, :cod, TRUNC(SYSDATE), :dataact, :nract,
|
||||
:expl, :scd, :scc, :suma, :proc_tva,
|
||||
:id_partc, :id_partd, :id_fdoc, :id_jtva, :taxcode, 0, SYSDATE,
|
||||
0, 0, 0
|
||||
)
|
||||
""",
|
||||
luna=luna, an=an, cod=cod, dataact=receipt_date, nract=nract,
|
||||
expl=line["expl"], scd=line["scd"], scc=line["scc"],
|
||||
suma=line["suma"], proc_tva=proc_tva,
|
||||
id_partc=line["id_partc"], id_partd=line["id_partd"],
|
||||
id_fdoc=id_fdoc, id_jtva=id_jtva, taxcode=taxcode
|
||||
)
|
||||
jtva_info = f" [JTVA={id_jtva}]" if id_jtva else ""
|
||||
taxcode_info = f" [TAX={taxcode}]" if taxcode else ""
|
||||
print(f" {line['scd']} = {line['scc']}: {line['suma']:.2f}{jtva_info}{taxcode_info}")
|
||||
|
||||
# 9. Finalize
|
||||
mesaj = cursor.var(oracledb.STRING, 4000)
|
||||
cursor.callproc('PACK_CONTAFIN.FINALIZEAZA_SCRIERE_ACT_RUL',
|
||||
[0, cod, 0, 0, 0, mesaj])
|
||||
|
||||
if do_commit:
|
||||
conn.commit()
|
||||
return {"success": True, "cod": cod, "luna": luna, "an": an, "saved": True,
|
||||
"id_part": id_part, "tva_incasare": tva_incasare}
|
||||
else:
|
||||
conn.rollback()
|
||||
return {"success": True, "cod": cod, "luna": luna, "an": an, "saved": False,
|
||||
"id_part": id_part, "tva_incasare": tva_incasare}
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
import traceback
|
||||
return {"success": False, "error": str(e), "traceback": traceback.format_exc()}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def process_bon(file_path: Path, do_save: bool = False, company_id: int = COMPANY_ID,
|
||||
api_user: str = API_USER, api_pass: str = API_PASS):
|
||||
"""Procesează un bon fiscal: OCR → SQLite → Oracle."""
|
||||
print("=" * 60)
|
||||
print(f"📄 Procesez: {file_path.name}")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. Login
|
||||
print("\n🔑 Login API...")
|
||||
client = APIClient(API_BASE)
|
||||
if not client.login(api_user, api_pass, SERVER_ID):
|
||||
print("❌ Login failed!")
|
||||
return None
|
||||
print(" ✅ OK")
|
||||
|
||||
# 2. OCR
|
||||
print("\n🔍 OCR extract...")
|
||||
ocr_result = client.ocr_extract(file_path)
|
||||
if not ocr_result["success"]:
|
||||
print(f" ❌ {ocr_result['error']}")
|
||||
return None
|
||||
|
||||
ocr = ocr_result["result"]
|
||||
print(f" ✅ OK ({ocr_result.get('time_ms', '?')}ms)")
|
||||
print(f" CUI: {ocr.get('cui')}")
|
||||
print(f" Partner: {ocr.get('partner_name')}")
|
||||
print(f" Data: {ocr.get('receipt_date')}")
|
||||
print(f" Total: {ocr.get('amount')} RON")
|
||||
print(f" TVA: {ocr.get('tva_total')} RON")
|
||||
|
||||
# Show payment methods
|
||||
payment_methods = ocr.get("payment_methods") or []
|
||||
if payment_methods:
|
||||
pm_str = ", ".join(f"{pm.get('method')}: {pm.get('amount')}" for pm in payment_methods)
|
||||
print(f" Plăți: {pm_str}")
|
||||
|
||||
# Show TVA breakdown
|
||||
tva_entries = ocr.get("tva_entries") or []
|
||||
if tva_entries:
|
||||
tva_str = ", ".join(f"{t.get('code')}({t.get('percent')}%): {t.get('amount')}" for t in tva_entries)
|
||||
print(f" TVA detaliat: {tva_str}")
|
||||
|
||||
# 3. SQLite (via API) - cu TOATE datele
|
||||
print("\n💾 Save SQLite (via API)...")
|
||||
sqlite_result = client.create_receipt(ocr, company_id)
|
||||
if not sqlite_result["success"]:
|
||||
print(f" ❌ {sqlite_result['error']}")
|
||||
return None
|
||||
|
||||
receipt = sqlite_result["receipt"]
|
||||
print(f" ✅ Receipt ID: {receipt.get('id')}")
|
||||
print(f" Payment mode: {receipt.get('payment_mode')}")
|
||||
|
||||
# 4. Oracle (direct)
|
||||
mode = "SAVE" if do_save else "DRY RUN"
|
||||
print(f"\n🗄️ Save Oracle ({mode})...")
|
||||
oracle_result = save_to_oracle(ocr, do_commit=do_save)
|
||||
|
||||
if oracle_result["success"]:
|
||||
if oracle_result["saved"]:
|
||||
print(f" ✅ SALVAT: COD={oracle_result['cod']}, {oracle_result['luna']:02d}/{oracle_result['an']}")
|
||||
else:
|
||||
print(f" ⚠️ DRY RUN: ar fi COD={oracle_result['cod']}")
|
||||
else:
|
||||
print(f" ❌ {oracle_result.get('error')}")
|
||||
if oracle_result.get("traceback"):
|
||||
print(oracle_result["traceback"])
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
return {
|
||||
"ocr": ocr,
|
||||
"sqlite_receipt_id": receipt.get("id"),
|
||||
"oracle": oracle_result
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Procesare bon fiscal: OCR → SQLite → Oracle")
|
||||
parser.add_argument("file", help="Path către PDF sau imagine")
|
||||
parser.add_argument("--save", action="store_true", help="Salvează efectiv în Oracle")
|
||||
parser.add_argument("--company", type=int, default=COMPANY_ID, help="Company ID")
|
||||
parser.add_argument("--user", default=API_USER, help="API username")
|
||||
parser.add_argument("--password", default=API_PASS, help="API password")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
file_path = Path(args.file)
|
||||
if not file_path.exists():
|
||||
print(f"❌ File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
result = process_bon(file_path, do_save=args.save, company_id=args.company,
|
||||
api_user=args.user, api_pass=args.password)
|
||||
|
||||
if result:
|
||||
print("\n✅ Done!")
|
||||
else:
|
||||
print("\n❌ Failed!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
488
tools/ralph_prd_generator.py
Normal file
488
tools/ralph_prd_generator.py
Normal file
@@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ralph PRD Generator - implementare Python a skill-ului ralph-prd
|
||||
Generează PRD și prd.json fără să apeleze Claude Code
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def detect_project_context(project_dir: Path):
|
||||
"""Detectează tech stack și mod (NEW_PROJECT vs FEATURE)"""
|
||||
context = {
|
||||
"mode": "NEW_PROJECT",
|
||||
"stack_type": None,
|
||||
"config_file": None,
|
||||
"scripts": {},
|
||||
"dependencies": []
|
||||
}
|
||||
|
||||
# Verifică fișiere de config
|
||||
config_files = {
|
||||
"package.json": "nodejs",
|
||||
"pyproject.toml": "python",
|
||||
"requirements.txt": "python-legacy",
|
||||
"go.mod": "go",
|
||||
"Cargo.toml": "rust",
|
||||
"pom.xml": "java-maven",
|
||||
"build.gradle": "java-gradle",
|
||||
"composer.json": "php",
|
||||
"Gemfile": "ruby"
|
||||
}
|
||||
|
||||
for filename, stack in config_files.items():
|
||||
config_path = project_dir / filename
|
||||
if config_path.exists():
|
||||
context["mode"] = "FEATURE"
|
||||
context["stack_type"] = stack
|
||||
context["config_file"] = filename
|
||||
|
||||
# Extrage info din package.json
|
||||
if filename == "package.json":
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
data = json.load(f)
|
||||
context["scripts"] = data.get("scripts", {})
|
||||
context["dependencies"] = list(data.get("dependencies", {}).keys())
|
||||
except:
|
||||
pass
|
||||
|
||||
break
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def generate_prd_markdown(project_name: str, description: str, context: dict):
|
||||
"""Generează PRD markdown bazat pe descriere și context"""
|
||||
|
||||
# Parse descriere pentru a extrage info
|
||||
lines = description.strip().split('\n')
|
||||
features = []
|
||||
requirements = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('Features:') or line.startswith('Feature:'):
|
||||
continue
|
||||
if line.startswith('-'):
|
||||
features.append(line[1:].strip())
|
||||
elif line and not line.endswith(':'):
|
||||
requirements.append(line)
|
||||
|
||||
if not features:
|
||||
# Dacă nu sunt features explicit, folosim descrierea ca feature
|
||||
features = [description.split('.')[0]]
|
||||
|
||||
# Detectează tip proiect din descriere
|
||||
desc_lower = description.lower()
|
||||
if 'cli' in desc_lower or 'command' in desc_lower:
|
||||
project_type = "CLI Tool"
|
||||
elif 'api' in desc_lower or 'backend' in desc_lower:
|
||||
project_type = "API / Backend"
|
||||
elif 'web' in desc_lower or 'app' in desc_lower:
|
||||
project_type = "Web Application"
|
||||
else:
|
||||
project_type = "Application"
|
||||
|
||||
# Detectează stack din descriere sau context
|
||||
if 'python' in desc_lower:
|
||||
stack = "Python"
|
||||
test_framework = "pytest"
|
||||
elif 'javascript' in desc_lower or 'node' in desc_lower or 'typescript' in desc_lower:
|
||||
stack = "Node.js / TypeScript"
|
||||
test_framework = "jest"
|
||||
elif context["stack_type"] == "nodejs":
|
||||
stack = "Node.js / TypeScript"
|
||||
test_framework = "jest"
|
||||
elif context["stack_type"] and context["stack_type"].startswith("python"):
|
||||
stack = "Python"
|
||||
test_framework = "pytest"
|
||||
else:
|
||||
stack = "Python" # Default
|
||||
test_framework = "pytest"
|
||||
|
||||
# Template PRD
|
||||
prd = f"""# PRD: {project_name.replace('-', ' ').title()}
|
||||
|
||||
## 1. Introducere
|
||||
|
||||
{description.split('.')[0]}.
|
||||
|
||||
**Data:** {datetime.now().strftime('%Y-%m-%d')}
|
||||
**Status:** Draft
|
||||
**Mode:** {context['mode']}
|
||||
|
||||
## 2. Context Tehnic
|
||||
|
||||
"""
|
||||
|
||||
if context["mode"] == "FEATURE":
|
||||
prd += f"""**Proiect existent detectat:**
|
||||
- Stack: {context['stack_type']}
|
||||
- Config: {context['config_file']}
|
||||
- Scripts: {', '.join(context['scripts'].keys())}
|
||||
|
||||
"""
|
||||
else:
|
||||
prd += f"""**Proiect nou:**
|
||||
- Tip: {project_type}
|
||||
- Stack recomandat: {stack}
|
||||
- Test framework: {test_framework}
|
||||
|
||||
"""
|
||||
|
||||
prd += f"""## 3. Obiective
|
||||
|
||||
### Obiectiv Principal
|
||||
{features[0] if features else description.split('.')[0]}
|
||||
|
||||
### Obiective Secundare
|
||||
"""
|
||||
|
||||
for i, feature in enumerate(features[1:] if len(features) > 1 else features, 1):
|
||||
prd += f"- {feature}\n"
|
||||
|
||||
prd += f"""
|
||||
### Metrici de Succes
|
||||
- Toate funcționalitățile implementate conform spec
|
||||
- Tests passing (coverage > 80%)
|
||||
- Code quality: lint + typecheck pass
|
||||
|
||||
## 4. User Stories
|
||||
|
||||
"""
|
||||
|
||||
# Generează user stories din features
|
||||
for i, feature in enumerate(features, 1):
|
||||
story_id = f"US-{i:03d}"
|
||||
title = feature.strip()
|
||||
|
||||
prd += f"""### {story_id}: {title}
|
||||
**Ca** utilizator
|
||||
**Vreau** {title.lower()}
|
||||
**Pentru că** pot folosi aplicația eficient
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Funcționalitatea implementată conform descrierii
|
||||
- [ ] Input validation în loc
|
||||
- [ ] Error handling pentru cazuri edge
|
||||
- [ ] Tests cu {test_framework} (coverage > 80%)
|
||||
- [ ] Code quality: lint + typecheck pass
|
||||
|
||||
**Priority:** {i * 10}
|
||||
|
||||
"""
|
||||
|
||||
# Dacă nu sunt multe features, adaugă story pentru tests
|
||||
if len(features) < 3:
|
||||
prd += f"""### US-{len(features)+1:03d}: Tests și Documentație
|
||||
**Ca** developer
|
||||
**Vreau** teste comprehensive și documentație
|
||||
**Pentru că** asigur calitatea codului
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Unit tests pentru toate funcțiile (coverage > 80%)
|
||||
- [ ] Integration tests pentru flow-uri principale
|
||||
- [ ] README cu instrucțiuni de utilizare
|
||||
- [ ] Docstrings pentru funcții publice
|
||||
- [ ] {test_framework} rulează fără erori
|
||||
|
||||
**Priority:** {(len(features)+1) * 10}
|
||||
|
||||
"""
|
||||
|
||||
prd += f"""## 5. Cerințe Funcționale
|
||||
|
||||
"""
|
||||
|
||||
for i, req in enumerate(requirements if requirements else features, 1):
|
||||
prd += f"{i}. [REQ-{i:03d}] {req}\n"
|
||||
|
||||
prd += f"""
|
||||
## 6. Non-Goals (Ce NU facem)
|
||||
|
||||
- Interfață grafică (GUI) - doar CLI/API
|
||||
- Suport multiple limbaje - doar {stack}
|
||||
- Deployment infrastructure - doar cod functional
|
||||
|
||||
## 7. Considerații Tehnice
|
||||
|
||||
### Stack/Tehnologii
|
||||
- Limbaj: {stack}
|
||||
- Testing: {test_framework}
|
||||
- Linting: pylint / eslint (depinde de stack)
|
||||
- Type checking: mypy / typescript
|
||||
|
||||
### Patterns de Urmat
|
||||
- Clean code principles
|
||||
- SOLID principles unde aplicabil
|
||||
- Error handling consistent
|
||||
- Input validation strict
|
||||
|
||||
### Riscuri Tehnice
|
||||
- Edge cases la input validation
|
||||
- Performance pentru volume mari de date (dacă aplicabil)
|
||||
|
||||
## 8. Considerații Security
|
||||
|
||||
- Input validation pentru toate datele externe
|
||||
- Error messages fără info sensibilă
|
||||
- Principle of least privilege
|
||||
|
||||
## 9. Open Questions
|
||||
|
||||
- [ ] Performance requirements specifice?
|
||||
- [ ] Limite pe input sizes?
|
||||
- [ ] Specific error handling patterns preferați?
|
||||
|
||||
---
|
||||
|
||||
**Generated by:** Echo (Ralph PRD Generator)
|
||||
**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
"""
|
||||
|
||||
return prd
|
||||
|
||||
|
||||
def prd_to_stories(prd_content: str, project_name: str):
|
||||
"""Extrage user stories din PRD și le convertește în format prd.json"""
|
||||
|
||||
stories = []
|
||||
|
||||
# Parse PRD pentru stories
|
||||
story_pattern = r'### (US-\d+): (.+?)\n\*\*Ca\*\* (.+?)\n\*\*Vreau\*\* (.+?)\n\*\*Pentru că\*\* (.+?)\n\n\*\*Acceptance Criteria:\*\*\n(.*?)\n\n\*\*Priority:\*\* (\d+)'
|
||||
|
||||
matches = re.finditer(story_pattern, prd_content, re.DOTALL)
|
||||
|
||||
for match in matches:
|
||||
story_id = match.group(1)
|
||||
title = match.group(2).strip()
|
||||
user_type = match.group(3).strip()
|
||||
want = match.group(4).strip()
|
||||
because = match.group(5).strip()
|
||||
criteria_text = match.group(6).strip()
|
||||
priority = int(match.group(7))
|
||||
|
||||
# Parse acceptance criteria
|
||||
criteria = []
|
||||
for line in criteria_text.split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith('- [ ]'):
|
||||
criteria.append(line[5:].strip())
|
||||
|
||||
# Detectează dacă necesită browser check (pentru UI)
|
||||
requires_browser = 'ui' in title.lower() or 'interface' in title.lower()
|
||||
|
||||
story = {
|
||||
"id": story_id,
|
||||
"title": title,
|
||||
"description": f"Ca {user_type}, vreau {want} pentru că {because}",
|
||||
"priority": priority,
|
||||
"acceptanceCriteria": criteria,
|
||||
"requiresBrowserCheck": requires_browser,
|
||||
"passes": False,
|
||||
"notes": ""
|
||||
}
|
||||
|
||||
stories.append(story)
|
||||
|
||||
# Dacă nu găsim stories (regex failed), generăm basic
|
||||
if not stories:
|
||||
stories = [{
|
||||
"id": "US-001",
|
||||
"title": "Implementare funcționalitate principală",
|
||||
"description": f"Implementează {project_name}",
|
||||
"priority": 10,
|
||||
"acceptanceCriteria": [
|
||||
"Funcționalitatea implementată",
|
||||
"Tests passing",
|
||||
"Lint + typecheck pass"
|
||||
],
|
||||
"requiresBrowserCheck": False,
|
||||
"passes": False,
|
||||
"notes": ""
|
||||
}]
|
||||
|
||||
return stories
|
||||
|
||||
|
||||
def detect_tech_stack_commands(project_dir: Path, context: dict):
|
||||
"""Detectează comenzile tech stack pentru prd.json"""
|
||||
|
||||
stack_type = context.get("stack_type", "python")
|
||||
scripts = context.get("scripts", {})
|
||||
|
||||
# Default commands per stack
|
||||
if stack_type == "nodejs" or "package.json" in str(context.get("config_file", "")):
|
||||
commands = {
|
||||
"start": scripts.get("dev", scripts.get("start", "npm run dev")),
|
||||
"build": scripts.get("build", "npm run build"),
|
||||
"lint": scripts.get("lint", "npm run lint"),
|
||||
"typecheck": scripts.get("typecheck", "npm run typecheck"),
|
||||
"test": scripts.get("test", "npm test")
|
||||
}
|
||||
port = 3000
|
||||
stack_name = "nextjs" if "next" in str(context.get("dependencies", [])) else "nodejs"
|
||||
|
||||
elif stack_type and stack_type.startswith("python"):
|
||||
commands = {
|
||||
"start": "python main.py",
|
||||
"build": "",
|
||||
"lint": "ruff check .",
|
||||
"typecheck": "mypy .",
|
||||
"test": "pytest"
|
||||
}
|
||||
port = 8000
|
||||
stack_name = "python"
|
||||
|
||||
else:
|
||||
# Generic/fallback
|
||||
commands = {
|
||||
"start": "python main.py",
|
||||
"build": "",
|
||||
"lint": "echo 'No linter configured'",
|
||||
"typecheck": "echo 'No typecheck configured'",
|
||||
"test": "pytest"
|
||||
}
|
||||
port = 8000
|
||||
stack_name = "python"
|
||||
|
||||
return {
|
||||
"type": stack_name,
|
||||
"commands": commands,
|
||||
"port": port
|
||||
}
|
||||
|
||||
|
||||
def create_prd_and_json(project_name: str, description: str, workspace_dir: Path):
|
||||
"""
|
||||
Generează PRD markdown și prd.json pentru un proiect
|
||||
|
||||
Returns:
|
||||
tuple: (prd_file_path, prd_json_path) sau (None, None) dacă eroare
|
||||
"""
|
||||
project_dir = workspace_dir / project_name
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Detectează context
|
||||
context = detect_project_context(project_dir)
|
||||
|
||||
print(f"📊 Context detectat:")
|
||||
print(f" Mode: {context['mode']}")
|
||||
if context['stack_type']:
|
||||
print(f" Stack: {context['stack_type']}")
|
||||
print(f" Config: {context['config_file']}")
|
||||
|
||||
# Generează PRD markdown
|
||||
prd_content = generate_prd_markdown(project_name, description, context)
|
||||
|
||||
# Salvează PRD
|
||||
tasks_dir = project_dir / "tasks"
|
||||
tasks_dir.mkdir(exist_ok=True)
|
||||
prd_file = tasks_dir / f"prd-{project_name}.md"
|
||||
|
||||
with open(prd_file, 'w', encoding='utf-8') as f:
|
||||
f.write(prd_content)
|
||||
|
||||
print(f"✅ PRD salvat: {prd_file}")
|
||||
|
||||
# Generează prd.json
|
||||
stories = prd_to_stories(prd_content, project_name)
|
||||
tech_stack = detect_tech_stack_commands(project_dir, context)
|
||||
|
||||
prd_json_data = {
|
||||
"projectName": project_name,
|
||||
"branchName": f"ralph/{project_name}",
|
||||
"description": description.split('\n')[0],
|
||||
"techStack": tech_stack,
|
||||
"userStories": stories
|
||||
}
|
||||
|
||||
# Creează structura ralph
|
||||
ralph_dir = project_dir / "scripts" / "ralph"
|
||||
ralph_dir.mkdir(parents=True, exist_ok=True)
|
||||
(ralph_dir / "logs").mkdir(exist_ok=True)
|
||||
(ralph_dir / "archive").mkdir(exist_ok=True)
|
||||
(ralph_dir / "screenshots").mkdir(exist_ok=True)
|
||||
|
||||
# Salvează prd.json
|
||||
prd_json_file = ralph_dir / "prd.json"
|
||||
with open(prd_json_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(prd_json_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✅ prd.json salvat: {prd_json_file}")
|
||||
print(f"📋 Stories: {len(stories)}")
|
||||
for story in stories:
|
||||
print(f" - {story['id']}: {story['title']}")
|
||||
|
||||
# Copiază template-uri ralph
|
||||
templates_dir = Path.home() / ".claude" / "skills" / "ralph" / "templates"
|
||||
if not templates_dir.exists():
|
||||
templates_dir = Path.home() / "clawd" / "skills" / "ralph" / "templates"
|
||||
|
||||
if templates_dir.exists():
|
||||
# Copiază ralph.sh
|
||||
ralph_sh_src = templates_dir / "ralph.sh"
|
||||
if ralph_sh_src.exists():
|
||||
ralph_sh_dst = ralph_dir / "ralph.sh"
|
||||
with open(ralph_sh_src) as f:
|
||||
content = f.read()
|
||||
with open(ralph_sh_dst, 'w') as f:
|
||||
f.write(content)
|
||||
ralph_sh_dst.chmod(0o755)
|
||||
print(f"✅ ralph.sh copiat")
|
||||
|
||||
# Copiază prompt.md
|
||||
prompt_src = templates_dir / "prompt.md"
|
||||
if prompt_src.exists():
|
||||
prompt_dst = ralph_dir / "prompt.md"
|
||||
with open(prompt_src) as f:
|
||||
content = f.read()
|
||||
with open(prompt_dst, 'w') as f:
|
||||
f.write(content)
|
||||
print(f"✅ prompt.md copiat")
|
||||
|
||||
# Init progress.txt
|
||||
progress_file = ralph_dir / "progress.txt"
|
||||
with open(progress_file, 'w') as f:
|
||||
f.write(f"# Ralph Progress Log\n")
|
||||
f.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
|
||||
f.write(f"Project: {project_name}\n")
|
||||
f.write(f"---\n")
|
||||
|
||||
print(f"✅ Structură Ralph completă în {ralph_dir}")
|
||||
|
||||
return prd_file, prd_json_file
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python ralph_prd_generator.py PROJECT_NAME 'description'")
|
||||
sys.exit(1)
|
||||
|
||||
project_name = sys.argv[1]
|
||||
description = sys.argv[2]
|
||||
workspace = Path.home() / "workspace"
|
||||
|
||||
print(f"🔄 Generez PRD pentru {project_name}")
|
||||
print("=" * 70)
|
||||
|
||||
prd_file, prd_json = create_prd_and_json(project_name, description, workspace)
|
||||
|
||||
if prd_file and prd_json:
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ PRD și prd.json generate cu succes!")
|
||||
print(f"📄 PRD: {prd_file}")
|
||||
print(f"📋 JSON: {prd_json}")
|
||||
print("\n📌 Următorii pași:")
|
||||
print(f" 1. Revizuiește PRD în {prd_file}")
|
||||
print(f" 2. Rulează Ralph: cd {prd_file.parent.parent} && ./scripts/ralph/ralph.sh 20")
|
||||
print("=" * 70)
|
||||
else:
|
||||
print("\n❌ Eroare la generare PRD")
|
||||
sys.exit(1)
|
||||
209
tools/ralph_workflow.py
Executable file
209
tools/ralph_workflow.py
Executable file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ralph Workflow Helper - pentru Echo
|
||||
Gestionează workflow-ul complet de creare și execuție proiecte cu Ralph + Claude Code
|
||||
Folosește ralph_prd_generator.py pentru PRD, apoi ralph.sh pentru implementare
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# Import generator PRD
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from ralph_prd_generator import create_prd_and_json
|
||||
|
||||
WORKSPACE = Path.home() / "workspace"
|
||||
|
||||
|
||||
def run_ralph(prd_json: Path, max_iterations: int = 20, background: bool = False):
|
||||
"""
|
||||
Rulează ralph.sh pentru execuție autonomă
|
||||
|
||||
Args:
|
||||
prd_json: Path către prd.json
|
||||
max_iterations: Max iterații Ralph
|
||||
background: Dacă True, rulează în background
|
||||
|
||||
Returns:
|
||||
subprocess.Popen object dacă background, altfel exit code
|
||||
"""
|
||||
project_dir = prd_json.parent.parent.parent
|
||||
ralph_script = prd_json.parent / "ralph.sh"
|
||||
|
||||
if not ralph_script.exists():
|
||||
print(f"❌ ralph.sh nu există în {ralph_script.parent}")
|
||||
return None
|
||||
|
||||
print(f"\n🚀 Lansez Ralph loop")
|
||||
print(f"📁 Project: {project_dir}")
|
||||
print(f"🔁 Max iterations: {max_iterations}")
|
||||
print(f"🌙 Background: {background}")
|
||||
|
||||
cmd = [str(ralph_script), str(max_iterations)]
|
||||
|
||||
if background:
|
||||
log_file = ralph_script.parent / "logs" / "ralph.log"
|
||||
log_file.parent.mkdir(exist_ok=True)
|
||||
|
||||
with open(log_file, 'w') as f:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=project_dir,
|
||||
stdout=f,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True
|
||||
)
|
||||
|
||||
print(f"✅ Ralph pornit în background (PID: {process.pid})")
|
||||
print(f"📋 Log: {log_file}")
|
||||
|
||||
pid_file = ralph_script.parent / ".ralph.pid"
|
||||
with open(pid_file, 'w') as f:
|
||||
f.write(str(process.pid))
|
||||
|
||||
return process
|
||||
else:
|
||||
print("⚠️ Rulare în foreground")
|
||||
result = subprocess.run(cmd, cwd=project_dir)
|
||||
return result.returncode
|
||||
|
||||
|
||||
def check_status(project_dir: Path):
|
||||
"""Verifică status Ralph pentru un proiect"""
|
||||
prd_json = project_dir / "scripts" / "ralph" / "prd.json"
|
||||
progress_file = project_dir / "scripts" / "ralph" / "progress.txt"
|
||||
pid_file = project_dir / "scripts" / "ralph" / ".ralph.pid"
|
||||
|
||||
status = {
|
||||
"project": project_dir.name,
|
||||
"running": False,
|
||||
"complete": [],
|
||||
"incomplete": [],
|
||||
"learnings": []
|
||||
}
|
||||
|
||||
if pid_file.exists():
|
||||
with open(pid_file) as f:
|
||||
pid = int(f.read().strip())
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
status["running"] = True
|
||||
status["pid"] = pid
|
||||
except OSError:
|
||||
status["running"] = False
|
||||
|
||||
if prd_json.exists():
|
||||
with open(prd_json) as f:
|
||||
data = json.load(f)
|
||||
for story in data.get('userStories', []):
|
||||
if story.get('passes'):
|
||||
status["complete"].append({
|
||||
"id": story['id'],
|
||||
"title": story['title']
|
||||
})
|
||||
else:
|
||||
status["incomplete"].append({
|
||||
"id": story['id'],
|
||||
"title": story['title'],
|
||||
"priority": story.get('priority', 999)
|
||||
})
|
||||
|
||||
if progress_file.exists():
|
||||
with open(progress_file) as f:
|
||||
lines = f.readlines()
|
||||
status["learnings"] = [l.strip() for l in lines[-10:] if l.strip()]
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI pentru testing"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:")
|
||||
print(" python ralph_workflow.py create PROJECT_NAME 'description'")
|
||||
print(" python ralph_workflow.py status PROJECT_NAME")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "create":
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python ralph_workflow.py create PROJECT_NAME 'description'")
|
||||
sys.exit(1)
|
||||
|
||||
project_name = sys.argv[2]
|
||||
description = sys.argv[3]
|
||||
|
||||
print("=" * 70)
|
||||
print(f"🧪 Ralph workflow: {project_name}")
|
||||
print("=" * 70)
|
||||
|
||||
# Generează PRD și prd.json
|
||||
prd_file, prd_json = create_prd_and_json(project_name, description, WORKSPACE)
|
||||
|
||||
if not prd_file or not prd_json:
|
||||
print("\n❌ Eroare la generare PRD")
|
||||
sys.exit(1)
|
||||
|
||||
# Lansează Ralph în background
|
||||
process = run_ralph(prd_json, max_iterations=20, background=True)
|
||||
|
||||
if process:
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ Workflow complet!")
|
||||
print(f"📁 Project: {prd_json.parent.parent.parent}")
|
||||
print(f"🔄 Ralph PID: {process.pid}")
|
||||
print(f"📋 Monitor: tail -f {prd_json.parent}/logs/ralph.log")
|
||||
print("=" * 70)
|
||||
else:
|
||||
print("\n❌ Eroare la lansare Ralph")
|
||||
sys.exit(1)
|
||||
|
||||
elif command == "status":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python ralph_workflow.py status PROJECT_NAME")
|
||||
sys.exit(1)
|
||||
|
||||
project_name = sys.argv[2]
|
||||
project_dir = WORKSPACE / project_name
|
||||
|
||||
if not project_dir.exists():
|
||||
print(f"❌ Proiect nu există: {project_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
status = check_status(project_dir)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"📊 Status: {status['project']}")
|
||||
print("=" * 70)
|
||||
print(f"🔄 Running: {'DA (PID: ' + str(status.get('pid', '')) + ')' if status['running'] else 'NU'}")
|
||||
print(f"✅ Complete: {len(status['complete'])}")
|
||||
print(f"🔄 Incomplete: {len(status['incomplete'])}")
|
||||
|
||||
if status['complete']:
|
||||
print("\n✅ Stories complete:")
|
||||
for s in status['complete'][:5]:
|
||||
print(f" - {s['id']}: {s['title']}")
|
||||
|
||||
if status['incomplete']:
|
||||
print("\n🔄 Stories incomplete:")
|
||||
for s in sorted(status['incomplete'], key=lambda x: x['priority'])[:5]:
|
||||
print(f" - {s['id']} (P{s['priority']}): {s['title']}")
|
||||
|
||||
if status['learnings']:
|
||||
print("\n📚 Recent learnings:")
|
||||
for l in status['learnings'][-5:]:
|
||||
print(f" {l}")
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
else:
|
||||
print(f"❌ Comandă necunoscută: {command}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
128
tools/security_audit.py
Executable file
128
tools/security_audit.py
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Security audit script - verifică zilnic:
|
||||
1. Permisiuni fișiere sensibile (600)
|
||||
2. Parole hardcoded în cod
|
||||
3. Fișiere sensibile în git tracking
|
||||
4. Porturi expuse neașteptat
|
||||
|
||||
Exit codes:
|
||||
0 = OK
|
||||
1 = Warnings found
|
||||
2 = Critical issues found
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
CLAWD_DIR = Path(__file__).parent.parent
|
||||
SENSITIVE_FILES = [".env", "credentials"]
|
||||
REQUIRED_PERMS = 0o600
|
||||
REQUIRED_DIR_PERMS = 0o700
|
||||
|
||||
issues = []
|
||||
warnings = []
|
||||
|
||||
def check_permissions():
|
||||
"""Check sensitive files have 600 permissions"""
|
||||
env_file = CLAWD_DIR / ".env"
|
||||
if env_file.exists():
|
||||
mode = env_file.stat().st_mode & 0o777
|
||||
if mode != REQUIRED_PERMS:
|
||||
issues.append(f".env has {oct(mode)} permissions (should be 0o600)")
|
||||
|
||||
creds_dir = CLAWD_DIR / "credentials"
|
||||
if creds_dir.exists():
|
||||
for f in creds_dir.iterdir():
|
||||
if f.is_file():
|
||||
mode = f.stat().st_mode & 0o777
|
||||
if mode != REQUIRED_PERMS:
|
||||
issues.append(f"credentials/{f.name} has {oct(mode)} (should be 0o600)")
|
||||
|
||||
def check_hardcoded_secrets():
|
||||
"""Scan Python files for potential hardcoded secrets"""
|
||||
patterns = [
|
||||
'password.*=.*"[^"]{4,}"',
|
||||
'api_key.*=.*"[^"]{8,}"',
|
||||
'secret.*=.*"[^"]{8,}"',
|
||||
]
|
||||
|
||||
for py_file in CLAWD_DIR.rglob("*.py"):
|
||||
if "venv" in str(py_file) or "__pycache__" in str(py_file):
|
||||
continue
|
||||
|
||||
try:
|
||||
content = py_file.read_text()
|
||||
for i, line in enumerate(content.split('\n'), 1):
|
||||
line_lower = line.lower()
|
||||
# Skip comments and env reads
|
||||
if line.strip().startswith('#') or 'os.getenv' in line or 'environ' in line:
|
||||
continue
|
||||
# Check for hardcoded passwords (excluding empty strings and placeholders)
|
||||
if ('password' in line_lower or 'api_pass' in line_lower) and '= "' in line and 'in line' not in line_lower:
|
||||
if '= ""' not in line and '= "***"' not in line:
|
||||
# Check if it's actually setting a value, not reading
|
||||
if 'getenv' not in line and 'environ' not in line:
|
||||
rel_path = py_file.relative_to(CLAWD_DIR)
|
||||
warnings.append(f"Potential hardcoded secret in {rel_path}:{i}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def check_git_tracking():
|
||||
"""Check if sensitive files are tracked by git"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "ls-files", ".env", "credentials/"],
|
||||
cwd=CLAWD_DIR,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.stdout.strip():
|
||||
for f in result.stdout.strip().split('\n'):
|
||||
issues.append(f"Sensitive file tracked by git: {f}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def check_gitignore():
|
||||
"""Verify .gitignore contains sensitive patterns"""
|
||||
gitignore = CLAWD_DIR / ".gitignore"
|
||||
if gitignore.exists():
|
||||
content = gitignore.read_text()
|
||||
required = [".env", "credentials/"]
|
||||
for pattern in required:
|
||||
if pattern not in content:
|
||||
warnings.append(f"Missing from .gitignore: {pattern}")
|
||||
|
||||
def main():
|
||||
print("🔒 Security Audit - Echo")
|
||||
print("=" * 40)
|
||||
|
||||
check_permissions()
|
||||
check_hardcoded_secrets()
|
||||
check_git_tracking()
|
||||
check_gitignore()
|
||||
|
||||
if issues:
|
||||
print("\n🔴 CRITICAL ISSUES:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
|
||||
if warnings:
|
||||
print("\n🟠 WARNINGS:")
|
||||
for warning in warnings:
|
||||
print(f" - {warning}")
|
||||
|
||||
if not issues and not warnings:
|
||||
print("\n✅ All checks passed!")
|
||||
return 0
|
||||
|
||||
print(f"\n📊 Summary: {len(issues)} critical, {len(warnings)} warnings")
|
||||
|
||||
if issues:
|
||||
return 2
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
289
tools/update_notes_index.py
Normal file
289
tools/update_notes_index.py
Normal file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generează index.json pentru KB din fișierele .md
|
||||
Scanează: memory/kb/, memory/, conversations/
|
||||
Extrage titlu, dată, tags, și domenii (@work, @health, etc.)
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent
|
||||
KB_ROOT = BASE_DIR / "memory" / "kb"
|
||||
MEMORY_DIR = BASE_DIR / "memory"
|
||||
CONVERSATIONS_DIR = BASE_DIR / "conversations"
|
||||
INDEX_FILE = KB_ROOT / "index.json"
|
||||
|
||||
# Domenii de agenți
|
||||
VALID_DOMAINS = ['work', 'health', 'growth', 'sprijin', 'scout']
|
||||
|
||||
# Tipuri speciale (pentru grup-sprijin etc.)
|
||||
VALID_TYPES = ['exercitiu', 'meditatie', 'reflectie', 'intrebare', 'fisa', 'project', 'memory', 'conversation', 'coaching']
|
||||
|
||||
# Cache for rules files
|
||||
_rules_cache = {}
|
||||
|
||||
def load_rules(filepath):
|
||||
"""Încarcă regulile din .rules.json din directorul fișierului sau părinți"""
|
||||
dir_path = filepath.parent
|
||||
|
||||
# Check cache
|
||||
if str(dir_path) in _rules_cache:
|
||||
return _rules_cache[str(dir_path)]
|
||||
|
||||
# Look for .rules.json in current dir and parents (up to memory/kb/)
|
||||
rules = {
|
||||
"defaultDomains": [],
|
||||
"defaultTypes": [],
|
||||
"defaultTags": [],
|
||||
"inferTypeFromFilename": False,
|
||||
"filenameTypeMap": {}
|
||||
}
|
||||
|
||||
# Collect rules from all levels (child rules override parent)
|
||||
rules_chain = []
|
||||
current = dir_path
|
||||
while current >= KB_ROOT:
|
||||
rules_file = current / ".rules.json"
|
||||
if rules_file.exists():
|
||||
try:
|
||||
with open(rules_file, 'r', encoding='utf-8') as f:
|
||||
rules_chain.insert(0, json.load(f)) # Parent first
|
||||
except:
|
||||
pass
|
||||
current = current.parent
|
||||
|
||||
# Merge rules (child overrides parent)
|
||||
for r in rules_chain:
|
||||
for key in rules:
|
||||
if key in r:
|
||||
if isinstance(rules[key], list):
|
||||
# Extend lists (don't override)
|
||||
rules[key] = list(set(rules[key] + r[key]))
|
||||
else:
|
||||
rules[key] = r[key]
|
||||
|
||||
_rules_cache[str(dir_path)] = rules
|
||||
return rules
|
||||
|
||||
def extract_metadata(filepath, category, subcategory=None):
|
||||
"""Extrage metadata din fișierul markdown"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extrage titlul (prima linie cu #)
|
||||
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
|
||||
title = title_match.group(1) if title_match else filepath.stem
|
||||
|
||||
# Extrage tags (linia cu **Tags:** sau tags:)
|
||||
tags = []
|
||||
domains = []
|
||||
types = []
|
||||
tags_match = re.search(r'\*\*Tags?:\*\*\s*(.+)$|^Tags?:\s*(.+)$', content, re.MULTILINE | re.IGNORECASE)
|
||||
if tags_match:
|
||||
tags_str = tags_match.group(1) or tags_match.group(2)
|
||||
|
||||
# Extrage domenii (@work, @health, etc.)
|
||||
domain_matches = re.findall(r'@(\w+)', tags_str)
|
||||
domains = [d for d in domain_matches if d in VALID_DOMAINS]
|
||||
types = [d for d in domain_matches if d in VALID_TYPES]
|
||||
|
||||
# Extrage tags normale (#tag)
|
||||
all_tags = re.findall(r'#([\w-]+)', tags_str)
|
||||
tags = [t for t in all_tags if t not in VALID_DOMAINS and t not in VALID_TYPES]
|
||||
|
||||
# Aplică reguli din .rules.json (dacă există)
|
||||
rules = load_rules(filepath)
|
||||
|
||||
# Adaugă domains implicite (dacă nu sunt deja)
|
||||
for d in rules.get("defaultDomains", []):
|
||||
if d not in domains:
|
||||
domains.append(d)
|
||||
|
||||
# Adaugă types implicite
|
||||
for t in rules.get("defaultTypes", []):
|
||||
if t not in types:
|
||||
types.append(t)
|
||||
|
||||
# Adaugă tags implicite
|
||||
for t in rules.get("defaultTags", []):
|
||||
if t not in tags:
|
||||
tags.append(t)
|
||||
|
||||
# Inferă type din filename (dacă e configurat)
|
||||
if rules.get("inferTypeFromFilename"):
|
||||
filename_lower = filepath.stem.lower()
|
||||
for pattern, type_name in rules.get("filenameTypeMap", {}).items():
|
||||
if pattern in filename_lower and type_name not in types:
|
||||
types.append(type_name)
|
||||
break
|
||||
|
||||
# Extrage data din filename (YYYY-MM-DD_slug.md sau YYYY-MM-DD.md)
|
||||
date_match = re.match(r'(\d{4}-\d{2}-\d{2})', filepath.name)
|
||||
date = date_match.group(1) if date_match else ""
|
||||
|
||||
# Pentru fișiere fără dată în nume, folosește mtime
|
||||
if not date:
|
||||
mtime = filepath.stat().st_mtime
|
||||
date = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d')
|
||||
|
||||
# Extrage video URL
|
||||
video_match = re.search(r'\*\*(?:Video|Link):\*\*\s*(https?://[^\s]+)', content)
|
||||
video_url = video_match.group(1) if video_match else ""
|
||||
|
||||
# Extrage TL;DR sau primele 200 caractere de conținut
|
||||
tldr = ""
|
||||
tldr_match = re.search(r'##\s*📋?\s*TL;DR\s*\n+(.+?)(?=\n##|\n---|\Z)', content, re.DOTALL)
|
||||
if tldr_match:
|
||||
tldr = tldr_match.group(1).strip()[:200]
|
||||
else:
|
||||
# Fallback: primul paragraf după titlu
|
||||
para_match = re.search(r'^#.+\n+(.+?)(?=\n\n|\n#|\Z)', content, re.DOTALL)
|
||||
if para_match:
|
||||
tldr = para_match.group(1).strip()[:200]
|
||||
if len(tldr) >= 200:
|
||||
tldr += "..."
|
||||
|
||||
# Construiește path-ul relativ pentru web (din dashboard/)
|
||||
# Dashboard are symlinks: notes-data -> ../kb, memory -> ../memory, conversations -> ../conversations
|
||||
rel_path = str(filepath.relative_to(BASE_DIR))
|
||||
# Transformă memory/kb/... în notes-data/... pentru web
|
||||
if rel_path.startswith('memory/kb/'):
|
||||
rel_path = 'notes-data/' + rel_path[10:] # strip 'memory/kb/'
|
||||
|
||||
return {
|
||||
"file": rel_path,
|
||||
"title": title,
|
||||
"date": date,
|
||||
"tags": tags,
|
||||
"domains": domains,
|
||||
"types": types,
|
||||
"category": category,
|
||||
"project": subcategory, # primul nivel sub projects/ (grup-sprijin, vending-master)
|
||||
"subdir": None, # se setează în scan_directory pentru niveluri mai adânci
|
||||
"video": video_url,
|
||||
"tldr": tldr
|
||||
}
|
||||
|
||||
def scan_directory(dir_path, category, subcategory=None, recursive=False):
|
||||
"""Scanează un director pentru fișiere .md"""
|
||||
notes = []
|
||||
|
||||
if not dir_path.exists():
|
||||
return notes
|
||||
|
||||
# Defaults pentru categorii speciale (memory/, conversations/)
|
||||
category_defaults = {
|
||||
"memory": {"types": ["memory"], "domains": []},
|
||||
"conversations": {"types": ["conversation"], "domains": []}
|
||||
}
|
||||
|
||||
if recursive:
|
||||
# Scanează recursiv
|
||||
for filepath in dir_path.rglob("*.md"):
|
||||
if filepath.name.startswith('.') or 'template' in filepath.name.lower():
|
||||
continue
|
||||
try:
|
||||
# Determină project și subdir din path
|
||||
# Ex: projects/grup-sprijin/biblioteca/file.md
|
||||
# project = grup-sprijin, subdir = biblioteca
|
||||
rel_to_dir = filepath.relative_to(dir_path)
|
||||
parts = rel_to_dir.parts[:-1] # exclude filename
|
||||
|
||||
project = parts[0] if len(parts) > 0 else None
|
||||
subdir = parts[1] if len(parts) > 1 else None
|
||||
|
||||
metadata = extract_metadata(filepath, category, project)
|
||||
metadata['subdir'] = subdir
|
||||
notes.append(metadata)
|
||||
except Exception as e:
|
||||
print(f" ! Error processing {filepath}: {e}")
|
||||
else:
|
||||
# Scanează doar fișierele din director (nu subdirectoare)
|
||||
for filepath in sorted(dir_path.glob("*.md"), reverse=True):
|
||||
if filepath.name.startswith('.') or 'template' in filepath.name.lower():
|
||||
continue
|
||||
try:
|
||||
metadata = extract_metadata(filepath, category, subcategory)
|
||||
# Aplică defaults pentru categoria specială
|
||||
if category in category_defaults:
|
||||
defaults = category_defaults[category]
|
||||
for t in defaults.get("types", []):
|
||||
if t not in metadata["types"]:
|
||||
metadata["types"].append(t)
|
||||
for d in defaults.get("domains", []):
|
||||
if d not in metadata["domains"]:
|
||||
metadata["domains"].append(d)
|
||||
notes.append(metadata)
|
||||
except Exception as e:
|
||||
print(f" ! Error processing {filepath}: {e}")
|
||||
|
||||
return notes
|
||||
|
||||
def generate_index():
|
||||
"""Generează index.json din toate sursele"""
|
||||
all_notes = []
|
||||
|
||||
# Stats
|
||||
domain_stats = {d: 0 for d in VALID_DOMAINS}
|
||||
category_stats = {}
|
||||
|
||||
# Scanează TOATE subdirectoarele din memory/kb/ recursiv
|
||||
print("Scanning memory/kb/ (all subdirectories)...")
|
||||
for subdir in sorted(KB_ROOT.iterdir()):
|
||||
if subdir.is_dir() and not subdir.name.startswith('.'):
|
||||
category = subdir.name
|
||||
print(f" [{category}]")
|
||||
notes = scan_directory(subdir, category, recursive=True)
|
||||
all_notes.extend(notes)
|
||||
category_stats[category] = len(notes)
|
||||
for n in notes:
|
||||
sub = f"/{n['subcategory']}" if n.get('subcategory') else ""
|
||||
print(f" + {n['title'][:42]}...")
|
||||
for d in n['domains']:
|
||||
domain_stats[d] += 1
|
||||
|
||||
# 4. Scanează memory/
|
||||
print("Scanning memory/...")
|
||||
memory_notes = scan_directory(MEMORY_DIR, "memory")
|
||||
all_notes.extend(memory_notes)
|
||||
category_stats["memory"] = len(memory_notes)
|
||||
for n in memory_notes:
|
||||
print(f" + {n['title'][:45]}...")
|
||||
|
||||
# 5. Scanează conversations/
|
||||
print("Scanning conversations/...")
|
||||
conv_notes = scan_directory(CONVERSATIONS_DIR, "conversations")
|
||||
all_notes.extend(conv_notes)
|
||||
category_stats["conversations"] = len(conv_notes)
|
||||
for n in conv_notes:
|
||||
print(f" + {n['title'][:45]}...")
|
||||
|
||||
# Sortează după dată descrescător
|
||||
all_notes.sort(key=lambda x: x['date'], reverse=True)
|
||||
|
||||
# Adaugă metadata globală
|
||||
output = {
|
||||
"notes": all_notes,
|
||||
"stats": {
|
||||
"total": len(all_notes),
|
||||
"by_domain": domain_stats,
|
||||
"by_category": category_stats
|
||||
},
|
||||
"domains": VALID_DOMAINS,
|
||||
"types": VALID_TYPES,
|
||||
"categories": list(category_stats.keys())
|
||||
}
|
||||
|
||||
with open(INDEX_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(output, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n✅ Generated {INDEX_FILE} with {len(all_notes)} notes")
|
||||
print(f" Categories: {category_stats}")
|
||||
return output
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_index()
|
||||
124
tools/youtube_subs.py
Executable file
124
tools/youtube_subs.py
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download YouTube subtitles/transcript for summarization.
|
||||
Usage: python3 youtube_subs.py <video_url> [language]
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def clean_vtt(content):
|
||||
"""Convert VTT to plain text, removing timestamps and duplicates."""
|
||||
lines = []
|
||||
seen = set()
|
||||
|
||||
for line in content.split('\n'):
|
||||
# Skip VTT headers, timestamps, positioning
|
||||
if line.startswith('WEBVTT') or line.startswith('Kind:') or line.startswith('Language:'):
|
||||
continue
|
||||
if '-->' in line: # Timestamp line
|
||||
continue
|
||||
if line.strip().startswith('<'): # Positioning tags
|
||||
continue
|
||||
if not line.strip():
|
||||
continue
|
||||
if re.match(r'^\d+$', line.strip()): # Sequence numbers
|
||||
continue
|
||||
|
||||
# Clean HTML tags
|
||||
clean = re.sub(r'<[^>]+>', '', line).strip()
|
||||
if clean and clean not in seen:
|
||||
seen.add(clean)
|
||||
lines.append(clean)
|
||||
|
||||
return ' '.join(lines)
|
||||
|
||||
def get_subtitles(url, lang='en'):
|
||||
"""Download subtitles for a YouTube video."""
|
||||
|
||||
yt_dlp = os.path.expanduser('~/.local/bin/yt-dlp')
|
||||
temp_dir = Path('/tmp/yt_subs')
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Clean old files
|
||||
for f in temp_dir.glob('*'):
|
||||
f.unlink()
|
||||
|
||||
# First, get video info
|
||||
info_cmd = [yt_dlp, '--dump-json', '--no-download', url]
|
||||
try:
|
||||
result = subprocess.run(info_cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0:
|
||||
info = json.loads(result.stdout)
|
||||
title = info.get('title', 'Unknown')
|
||||
duration = info.get('duration', 0)
|
||||
print(f"Title: {title}", file=sys.stderr)
|
||||
print(f"Duration: {duration//60}:{duration%60:02d}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
title = "Unknown"
|
||||
print(f"Could not get video info: {e}", file=sys.stderr)
|
||||
|
||||
# Try to get subtitles in order of preference
|
||||
lang_preferences = [lang, 'ro', 'en', 'en-US', 'en-GB']
|
||||
|
||||
for try_lang in lang_preferences:
|
||||
# Try manual subtitles first
|
||||
cmd = [
|
||||
yt_dlp,
|
||||
'--write-subs',
|
||||
'--sub-langs', try_lang,
|
||||
'--skip-download',
|
||||
'-o', str(temp_dir / '%(id)s.%(ext)s'),
|
||||
url
|
||||
]
|
||||
|
||||
subprocess.run(cmd, capture_output=True, timeout=60)
|
||||
|
||||
# Check if we got subtitles
|
||||
for ext in ['vtt', 'srt', 'ass']:
|
||||
for sub_file in temp_dir.glob(f'*.{try_lang}*.{ext}'):
|
||||
content = sub_file.read_text(encoding='utf-8', errors='replace')
|
||||
return title, clean_vtt(content)
|
||||
|
||||
# Try auto-generated subtitles
|
||||
for try_lang in lang_preferences:
|
||||
cmd = [
|
||||
yt_dlp,
|
||||
'--write-auto-subs',
|
||||
'--sub-langs', try_lang,
|
||||
'--skip-download',
|
||||
'-o', str(temp_dir / '%(id)s.%(ext)s'),
|
||||
url
|
||||
]
|
||||
|
||||
subprocess.run(cmd, capture_output=True, timeout=60)
|
||||
|
||||
for ext in ['vtt', 'srt', 'ass']:
|
||||
for sub_file in temp_dir.glob(f'*.{ext}'):
|
||||
content = sub_file.read_text(encoding='utf-8', errors='replace')
|
||||
text = clean_vtt(content)
|
||||
if text:
|
||||
return title, text
|
||||
|
||||
return title, None
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 youtube_subs.py <video_url> [language]")
|
||||
sys.exit(1)
|
||||
|
||||
url = sys.argv[1]
|
||||
lang = sys.argv[2] if len(sys.argv) > 2 else 'en'
|
||||
|
||||
title, transcript = get_subtitles(url, lang)
|
||||
|
||||
if transcript:
|
||||
print(f"\n=== {title} ===\n")
|
||||
print(transcript)
|
||||
else:
|
||||
print(f"No subtitles found for: {title}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user