oneCInteraction 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- onecinteraction-1.0.0/LICENSE +21 -0
- onecinteraction-1.0.0/PKG-INFO +54 -0
- onecinteraction-1.0.0/README.md +39 -0
- onecinteraction-1.0.0/pyproject.toml +22 -0
- onecinteraction-1.0.0/setup.cfg +4 -0
- onecinteraction-1.0.0/src/oneCInteraction/__init__.py +21 -0
- onecinteraction-1.0.0/src/oneCInteraction/characteristics.py +109 -0
- onecinteraction-1.0.0/src/oneCInteraction/connection.py +98 -0
- onecinteraction-1.0.0/src/oneCInteraction/groups.py +153 -0
- onecinteraction-1.0.0/src/oneCInteraction/log.py +99 -0
- onecinteraction-1.0.0/src/oneCInteraction/nomenclature.py +481 -0
- onecinteraction-1.0.0/src/oneCInteraction/orders.py +320 -0
- onecinteraction-1.0.0/src/oneCInteraction/structures.py +115 -0
- onecinteraction-1.0.0/src/oneCInteraction.egg-info/PKG-INFO +54 -0
- onecinteraction-1.0.0/src/oneCInteraction.egg-info/SOURCES.txt +17 -0
- onecinteraction-1.0.0/src/oneCInteraction.egg-info/dependency_links.txt +1 -0
- onecinteraction-1.0.0/src/oneCInteraction.egg-info/requires.txt +4 -0
- onecinteraction-1.0.0/src/oneCInteraction.egg-info/top_level.txt +1 -0
- onecinteraction-1.0.0/tests/test_lib.py +40 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agcl-x
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oneCInteraction
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python library for interacting with 1C:Enterprise databases via COM connection.
|
|
5
|
+
Author: agcl
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pytz
|
|
13
|
+
Requires-Dist: pywin32; platform_system == "Windows"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# 1C Interaction Library (`onec_interaction`)
|
|
17
|
+
|
|
18
|
+
A modular, clean Python library for interacting with 1C:Enterprise databases using Windows COM connection (`V83.COMConnector`).
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install the package locally in editable mode:
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from onec_interaction import Connection, Customer, Order, OrderItem
|
|
31
|
+
|
|
32
|
+
# 1. Initialize the connection details
|
|
33
|
+
c_conn = Connection(
|
|
34
|
+
s_oneCDatabasePathIn="C:\\Path\\To\\1C\\Database",
|
|
35
|
+
s_usernameIn="admin",
|
|
36
|
+
s_passwordIn="password"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 2. Configure default parameters
|
|
40
|
+
c_conn.s_warehouse_code = "WH001"
|
|
41
|
+
c_conn.s_counteragent_code = "CA001"
|
|
42
|
+
c_conn.s_organisation_code = "ORG001"
|
|
43
|
+
|
|
44
|
+
# 3. Establish COM connection
|
|
45
|
+
c_conn.initiate_connection()
|
|
46
|
+
|
|
47
|
+
# 4. Use composition managers
|
|
48
|
+
# Fetch product details
|
|
49
|
+
c_product = c_conn.nomenclature.get(s_articleIn="ART001")
|
|
50
|
+
print(f"Product: {c_product.s_name}, Price: {c_product.l_variety[0].n_price}")
|
|
51
|
+
|
|
52
|
+
# Close connection when done
|
|
53
|
+
c_conn.close_connection()
|
|
54
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 1C Interaction Library (`onec_interaction`)
|
|
2
|
+
|
|
3
|
+
A modular, clean Python library for interacting with 1C:Enterprise databases using Windows COM connection (`V83.COMConnector`).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the package locally in editable mode:
|
|
8
|
+
```bash
|
|
9
|
+
pip install -e .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from onec_interaction import Connection, Customer, Order, OrderItem
|
|
16
|
+
|
|
17
|
+
# 1. Initialize the connection details
|
|
18
|
+
c_conn = Connection(
|
|
19
|
+
s_oneCDatabasePathIn="C:\\Path\\To\\1C\\Database",
|
|
20
|
+
s_usernameIn="admin",
|
|
21
|
+
s_passwordIn="password"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# 2. Configure default parameters
|
|
25
|
+
c_conn.s_warehouse_code = "WH001"
|
|
26
|
+
c_conn.s_counteragent_code = "CA001"
|
|
27
|
+
c_conn.s_organisation_code = "ORG001"
|
|
28
|
+
|
|
29
|
+
# 3. Establish COM connection
|
|
30
|
+
c_conn.initiate_connection()
|
|
31
|
+
|
|
32
|
+
# 4. Use composition managers
|
|
33
|
+
# Fetch product details
|
|
34
|
+
c_product = c_conn.nomenclature.get(s_articleIn="ART001")
|
|
35
|
+
print(f"Product: {c_product.s_name}, Price: {c_product.l_variety[0].n_price}")
|
|
36
|
+
|
|
37
|
+
# Close connection when done
|
|
38
|
+
c_conn.close_connection()
|
|
39
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oneCInteraction"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A Python library for interacting with 1C:Enterprise databases via COM connection."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "agcl" }
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: Microsoft :: Windows",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"pytz",
|
|
21
|
+
"pywin32; platform_system == 'Windows'"
|
|
22
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .connection import Connection
|
|
2
|
+
from .structures import (
|
|
3
|
+
Nomenclature,
|
|
4
|
+
Variety,
|
|
5
|
+
Characteristic,
|
|
6
|
+
Group,
|
|
7
|
+
Customer,
|
|
8
|
+
OrderItem,
|
|
9
|
+
Order
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'Connection',
|
|
14
|
+
'Nomenclature',
|
|
15
|
+
'Variety',
|
|
16
|
+
'Characteristic',
|
|
17
|
+
'Group',
|
|
18
|
+
'Customer',
|
|
19
|
+
'OrderItem',
|
|
20
|
+
'Order'
|
|
21
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from .log import log_sys
|
|
2
|
+
from . import structures
|
|
3
|
+
|
|
4
|
+
class CharacteristicsManager:
|
|
5
|
+
def __init__(self, c_connectionIn):
|
|
6
|
+
self.c_connection = c_connectionIn
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
def c_v8(self):
|
|
10
|
+
return self.c_connection.c_v8
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def parse_name(s_charNameIn: str) -> list:
|
|
14
|
+
"""Parses a characteristic name string into a list of Characteristic objects."""
|
|
15
|
+
if not s_charNameIn or s_charNameIn in ["NULL", "Без характеристики"]:
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
if " - " in s_charNameIn:
|
|
19
|
+
l_parts = s_charNameIn.split(" - ")
|
|
20
|
+
s_value = l_parts[1] if len(l_parts) > 1 else l_parts[0]
|
|
21
|
+
return [structures.Characteristic("Характеристика", s_value)]
|
|
22
|
+
elif ":" in s_charNameIn:
|
|
23
|
+
l_parts = s_charNameIn.split(":")
|
|
24
|
+
s_name = l_parts[0].strip()
|
|
25
|
+
s_value = l_parts[1].strip() if len(l_parts) > 1 else ""
|
|
26
|
+
return [structures.Characteristic(s_name, s_value)]
|
|
27
|
+
else:
|
|
28
|
+
return [structures.Characteristic("Характеристика", s_charNameIn)]
|
|
29
|
+
|
|
30
|
+
def get(self, c_charRefIn, s_charNameIn: str = "") -> list:
|
|
31
|
+
"""Fetches characteristic properties from registry or parses description."""
|
|
32
|
+
l_characteristics = []
|
|
33
|
+
if c_charRefIn is not None and not c_charRefIn.IsEmpty():
|
|
34
|
+
try:
|
|
35
|
+
c_query = self.c_v8.NewObject("Query")
|
|
36
|
+
c_query.Text = """
|
|
37
|
+
SELECT
|
|
38
|
+
Properties.Свойство.Наименование AS PropName,
|
|
39
|
+
Properties.Значение.Наименование AS ValName
|
|
40
|
+
FROM
|
|
41
|
+
РегистрСведений.ЗначенияСвойствОбъектов AS Properties
|
|
42
|
+
WHERE
|
|
43
|
+
Properties.Объект = &CharRef
|
|
44
|
+
"""
|
|
45
|
+
c_query.SetParameter("CharRef", c_charRefIn)
|
|
46
|
+
c_res = c_query.Execute()
|
|
47
|
+
if c_res is not None and not c_res.IsEmpty():
|
|
48
|
+
c_sel = c_res.Select()
|
|
49
|
+
while c_sel.Next():
|
|
50
|
+
l_characteristics.append(structures.Characteristic(c_sel.PropName, c_sel.ValName))
|
|
51
|
+
except Exception as e:
|
|
52
|
+
log_sys(f"Error fetching characteristics for {s_charNameIn}: {e}", 1)
|
|
53
|
+
|
|
54
|
+
# Fallback to string parsing if properties were not found in registry
|
|
55
|
+
if not l_characteristics:
|
|
56
|
+
l_characteristics = self.parse_name(s_charNameIn)
|
|
57
|
+
|
|
58
|
+
return l_characteristics
|
|
59
|
+
|
|
60
|
+
def get_variety(self, c_charRefIn, s_charNameIn: str, n_priceIn: float, n_priceOptIn: float, d_stocksIn: dict):
|
|
61
|
+
"""Creates a Variety object with its characteristics and stock counts."""
|
|
62
|
+
l_characteristics = self.get(c_charRefIn, s_charNameIn)
|
|
63
|
+
return structures.Variety(
|
|
64
|
+
n_priceIn=n_priceIn,
|
|
65
|
+
n_priceOptIn=n_priceOptIn,
|
|
66
|
+
d_countIn=d_stocksIn,
|
|
67
|
+
l_characteristicsIn=l_characteristics
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def fetch_batch(self, l_charRefsIn: list) -> dict:
|
|
71
|
+
"""Batch fetches properties and values for a list of characteristics."""
|
|
72
|
+
if not self.c_v8 or not l_charRefsIn:
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
log_sys(f"Batch fetching properties for {len(l_charRefsIn)} characteristics...")
|
|
76
|
+
|
|
77
|
+
c_charRefsV8 = self.c_v8.NewObject("ValueList")
|
|
78
|
+
for c_ref in l_charRefsIn:
|
|
79
|
+
if c_ref and not c_ref.IsEmpty():
|
|
80
|
+
c_charRefsV8.Add(c_ref)
|
|
81
|
+
|
|
82
|
+
d_charProps = {} # {char_uuid: [Characteristic]}
|
|
83
|
+
|
|
84
|
+
if c_charRefsV8.Count() > 0:
|
|
85
|
+
try:
|
|
86
|
+
c_propQuery = self.c_v8.NewObject("Query")
|
|
87
|
+
c_propQuery.Text = """
|
|
88
|
+
SELECT
|
|
89
|
+
Properties.Объект AS CharRef,
|
|
90
|
+
Properties.Свойство.Наименование AS PropName,
|
|
91
|
+
Properties.Значение.Наименование AS ValName
|
|
92
|
+
FROM
|
|
93
|
+
РегистрСведений.ЗначенияСвойствОбъектов AS Properties
|
|
94
|
+
WHERE
|
|
95
|
+
Properties.Объект В (&CharRefs)
|
|
96
|
+
"""
|
|
97
|
+
c_propQuery.SetParameter("CharRefs", c_charRefsV8)
|
|
98
|
+
c_propRes = c_propQuery.Execute()
|
|
99
|
+
if c_propRes is not None and not c_propRes.IsEmpty():
|
|
100
|
+
c_sel = c_propRes.Select()
|
|
101
|
+
while c_sel.Next():
|
|
102
|
+
s_uuid = self.c_v8.String(c_sel.CharRef.UUID())
|
|
103
|
+
if s_uuid not in d_charProps:
|
|
104
|
+
d_charProps[s_uuid] = []
|
|
105
|
+
d_charProps[s_uuid].append(structures.Characteristic(c_sel.PropName, c_sel.ValName))
|
|
106
|
+
except Exception as e:
|
|
107
|
+
log_sys(f"Error batch fetching characteristics: {e}", 1)
|
|
108
|
+
|
|
109
|
+
return d_charProps
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import pythoncom
|
|
3
|
+
import pytz
|
|
4
|
+
from .log import log_sys
|
|
5
|
+
|
|
6
|
+
# Handle path for win32com on local environments
|
|
7
|
+
PYWIN32_PATH = r'C:\Users\Администратор\AppData\Local\Programs\Python\Python314\Lib\site-packages\pywin32_system32'
|
|
8
|
+
if PYWIN32_PATH not in sys.path:
|
|
9
|
+
sys.path.append(PYWIN32_PATH)
|
|
10
|
+
|
|
11
|
+
import win32com.client
|
|
12
|
+
|
|
13
|
+
from .nomenclature import NomenclatureManager
|
|
14
|
+
from .groups import GroupsManager
|
|
15
|
+
from .orders import OrdersManager
|
|
16
|
+
from .characteristics import CharacteristicsManager
|
|
17
|
+
|
|
18
|
+
class Connection:
|
|
19
|
+
"""Manages the COM connection to 1C and hosts managers to interact with different modules."""
|
|
20
|
+
|
|
21
|
+
tz_kiev = pytz.timezone('Europe/Kiev')
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
s_oneCDatabasePathIn: str,
|
|
26
|
+
s_usernameIn: str,
|
|
27
|
+
s_passwordIn: str
|
|
28
|
+
):
|
|
29
|
+
"""Initializes connection details, saves required codes, and instantiates managers."""
|
|
30
|
+
self.s_connection_string = f"File='{s_oneCDatabasePathIn}';Usr='{s_usernameIn}';Pwd='{s_passwordIn}';"
|
|
31
|
+
self.c_v8 = None
|
|
32
|
+
self.d_price_types = {}
|
|
33
|
+
|
|
34
|
+
# Save codes to self for order creation
|
|
35
|
+
self.s_warehouse_code = ""
|
|
36
|
+
self.s_counteragent_code = ""
|
|
37
|
+
self.s_organisation_code = ""
|
|
38
|
+
self.sl_price_types = ["Розничная", "Оптовая"]
|
|
39
|
+
|
|
40
|
+
# Managers (Composition)
|
|
41
|
+
self.nomenclature = NomenclatureManager(self)
|
|
42
|
+
self.groups = GroupsManager(self)
|
|
43
|
+
self.orders = OrdersManager(self)
|
|
44
|
+
self.characteristics = CharacteristicsManager(self)
|
|
45
|
+
|
|
46
|
+
def initiate_connection(self) -> None:
|
|
47
|
+
"""Establishes COM connection to 1C and caches price type references."""
|
|
48
|
+
log_sys('Trying to initiate connection...')
|
|
49
|
+
try:
|
|
50
|
+
pythoncom.CoInitialize()
|
|
51
|
+
c_connector = win32com.client.Dispatch("V83.COMConnector")
|
|
52
|
+
self.c_v8 = c_connector.Connect(self.s_connection_string)
|
|
53
|
+
log_sys("Successfully connected to 1C.")
|
|
54
|
+
self._cache_price_types()
|
|
55
|
+
except Exception as e:
|
|
56
|
+
log_sys(f"Failed to connect to 1C: {e}")
|
|
57
|
+
self.c_v8 = None
|
|
58
|
+
|
|
59
|
+
def _cache_price_types(self) -> None:
|
|
60
|
+
"""Caches references for retail and wholesale price types from 1C catalog."""
|
|
61
|
+
if not self.c_v8:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
log_sys("Caching price types...")
|
|
65
|
+
for s_ptName in self.sl_price_types:
|
|
66
|
+
self.get_price_type_ref(s_ptName)
|
|
67
|
+
|
|
68
|
+
def get_price_type_ref(self, s_nameIn: str):
|
|
69
|
+
"""Returns the cached price type reference, or queries 1C if not yet cached."""
|
|
70
|
+
if s_nameIn in self.d_price_types:
|
|
71
|
+
return self.d_price_types[s_nameIn]
|
|
72
|
+
|
|
73
|
+
if not self.c_v8:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
c_query = self.c_v8.NewObject("Query")
|
|
78
|
+
c_query.Text = "SELECT Ссылка FROM Справочник.ТипыЦенНоменклатуры WHERE Наименование = &Name"
|
|
79
|
+
c_query.SetParameter("Name", s_nameIn)
|
|
80
|
+
c_ptExec = c_query.Execute()
|
|
81
|
+
if c_ptExec is not None and not c_ptExec.IsEmpty():
|
|
82
|
+
c_ptRes = c_ptExec.Select()
|
|
83
|
+
c_ptRes.Next()
|
|
84
|
+
self.d_price_types[s_nameIn] = c_ptRes.Ссылка
|
|
85
|
+
return c_ptRes.Ссылка
|
|
86
|
+
else:
|
|
87
|
+
log_sys(f"Cannot find price type '{s_nameIn}' in 1C constants/catalog", 1)
|
|
88
|
+
return self.c_v8.String("")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
log_sys(f"Error fetching price type '{s_nameIn}': {e}", 1)
|
|
91
|
+
return self.c_v8.String("")
|
|
92
|
+
|
|
93
|
+
def close_connection(self) -> None:
|
|
94
|
+
"""Closes the COM connection and uninitializes PythonCOM."""
|
|
95
|
+
log_sys("Closing 1C connection...")
|
|
96
|
+
self.c_v8 = None
|
|
97
|
+
pythoncom.CoUninitialize()
|
|
98
|
+
log_sys("1C connection closed successfully.")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from .log import log_sys
|
|
2
|
+
from . import structures
|
|
3
|
+
|
|
4
|
+
class GroupsManager:
|
|
5
|
+
def __init__(self, c_connectionIn):
|
|
6
|
+
self.c_connection = c_connectionIn
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
def c_v8(self):
|
|
10
|
+
return self.c_connection.c_v8
|
|
11
|
+
|
|
12
|
+
def get_tree(self, sl_ignoredCategoriesNamesIn: list) -> list:
|
|
13
|
+
"""Retrieves and builds hierarchical group structure excluding ignored names."""
|
|
14
|
+
if not self.c_v8:
|
|
15
|
+
log_sys("Failed to get hierarchical groups: No connection to 1C.", 1)
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
c_ignoredVl = self.c_v8.NewObject("ValueList")
|
|
20
|
+
|
|
21
|
+
if sl_ignoredCategoriesNamesIn:
|
|
22
|
+
log_sys(f"Finding references for ignored categories: {sl_ignoredCategoriesNamesIn}")
|
|
23
|
+
c_refQuery = self.c_v8.NewObject("Query")
|
|
24
|
+
|
|
25
|
+
l_whereClauses = []
|
|
26
|
+
for i in range(len(sl_ignoredCategoriesNamesIn)):
|
|
27
|
+
l_whereClauses.append(f"Наименование = &Name{i}")
|
|
28
|
+
|
|
29
|
+
c_refQuery.Text = f"SELECT Ссылка FROM Справочник.Номенклатура WHERE ({' OR '.join(l_whereClauses)}) AND ЭтоГруппа = ИСТИНА"
|
|
30
|
+
|
|
31
|
+
for i, s_name in enumerate(sl_ignoredCategoriesNamesIn):
|
|
32
|
+
c_refQuery.SetParameter(f"Name{i}", s_name)
|
|
33
|
+
|
|
34
|
+
c_refRes = c_refQuery.Execute()
|
|
35
|
+
if not c_refRes.IsEmpty():
|
|
36
|
+
c_sel = c_refRes.Select()
|
|
37
|
+
while c_sel.Next():
|
|
38
|
+
c_ignoredVl.Add(c_sel.Ссылка)
|
|
39
|
+
|
|
40
|
+
log_sys("Fetching nomenclature groups (excluding ignored branches)...")
|
|
41
|
+
c_query = self.c_v8.NewObject("Query")
|
|
42
|
+
s_queryText = """
|
|
43
|
+
SELECT Ссылка AS Ref,
|
|
44
|
+
Наименование AS Name,
|
|
45
|
+
Код AS Code,
|
|
46
|
+
Родитель AS Parent
|
|
47
|
+
FROM Справочник.Номенклатура
|
|
48
|
+
WHERE ЭтоГруппа = ИСТИНА AND ПометкаУдаления = ЛОЖЬ
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if c_ignoredVl.Count() > 0:
|
|
52
|
+
s_queryText += " AND NOT Ссылка В ИЕРАРХИИ(&IgnoredRefs)"
|
|
53
|
+
c_query.SetParameter("IgnoredRefs", c_ignoredVl)
|
|
54
|
+
|
|
55
|
+
s_queryText += " ORDER BY Родитель, Наименование"
|
|
56
|
+
c_query.Text = s_queryText
|
|
57
|
+
|
|
58
|
+
c_result = c_query.Execute()
|
|
59
|
+
|
|
60
|
+
d_groupsDict = {}
|
|
61
|
+
l_rootGroups = []
|
|
62
|
+
|
|
63
|
+
if not c_result.IsEmpty():
|
|
64
|
+
c_selection = c_result.Select()
|
|
65
|
+
while c_selection.Next():
|
|
66
|
+
c_groupRef = c_selection.Ref
|
|
67
|
+
s_groupName = c_selection.Name
|
|
68
|
+
s_groupCode = self.c_v8.String(c_selection.Code)
|
|
69
|
+
c_parentRef = c_selection.Parent
|
|
70
|
+
|
|
71
|
+
s_refKey = self.c_v8.String(c_groupRef.UUID())
|
|
72
|
+
c_groupObj = structures.Group(
|
|
73
|
+
s_groupNameIn=s_groupName,
|
|
74
|
+
l_nomenclaturesIn=[],
|
|
75
|
+
c_refIn=c_groupRef,
|
|
76
|
+
s_codeIn=s_groupCode,
|
|
77
|
+
s_uuidIn=s_refKey
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
s_parentKey = self.c_v8.String(c_parentRef.UUID()) if not c_parentRef.IsEmpty() else None
|
|
81
|
+
|
|
82
|
+
d_groupsDict[s_refKey] = {
|
|
83
|
+
'obj': c_groupObj,
|
|
84
|
+
'parent': s_parentKey
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for s_ref, d_data in d_groupsDict.items():
|
|
88
|
+
s_parentKey = d_data['parent']
|
|
89
|
+
if s_parentKey and s_parentKey in d_groupsDict:
|
|
90
|
+
d_groupsDict[s_parentKey]['obj'].l_subGroups.append(d_data['obj'])
|
|
91
|
+
else:
|
|
92
|
+
l_rootGroups.append(d_data['obj'])
|
|
93
|
+
|
|
94
|
+
log_sys(f"Successfully fetched and organized {len(d_groupsDict)} groups.")
|
|
95
|
+
return l_rootGroups
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
log_sys(f"Error in getHierarchicalGroups: {e}", 1)
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
def get_by_name(self, s_nameIn: str):
|
|
102
|
+
"""Finds a single category Group by its name."""
|
|
103
|
+
if not self.c_v8:
|
|
104
|
+
log_sys("Failed to get category: No connection to 1C. Returning None", 1)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
log_sys(f"Trying to get category by name: {s_nameIn}...")
|
|
109
|
+
c_query = self.c_v8.NewObject("Query")
|
|
110
|
+
c_query.Text = """
|
|
111
|
+
SELECT TOP 1 Ссылка AS Ref,
|
|
112
|
+
Наименование AS Name,
|
|
113
|
+
Код AS Code
|
|
114
|
+
FROM Справочник.Номенклатура
|
|
115
|
+
WHERE Наименование = &Name AND ЭтоГруппа = ИСТИНА AND ПометкаУдаления = ЛОЖЬ
|
|
116
|
+
"""
|
|
117
|
+
c_query.SetParameter("Name", s_nameIn)
|
|
118
|
+
|
|
119
|
+
c_result = c_query.Execute()
|
|
120
|
+
if c_result.IsEmpty():
|
|
121
|
+
log_sys(f"Category '{s_nameIn}' not found. Returning None", 1)
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
c_selection = c_result.Select()
|
|
125
|
+
c_selection.Next()
|
|
126
|
+
|
|
127
|
+
s_groupCode = self.c_v8.String(c_selection.Code)
|
|
128
|
+
log_sys(f"Category '{s_nameIn}' successfully found.")
|
|
129
|
+
return structures.Group(s_groupNameIn=c_selection.Name, l_nomenclaturesIn=[], c_refIn=c_selection.Ref, s_codeIn=s_groupCode)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
log_sys(f"Error in getCategoryByName: {e}", 1)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def get_full_path(self, c_groupRefIn) -> str:
|
|
135
|
+
"""Returns the full hierarchical path of the group (e.g. 'Parent > Subgroup')."""
|
|
136
|
+
if c_groupRefIn is None or c_groupRefIn.IsEmpty():
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
l_pathParts = []
|
|
140
|
+
c_currentRef = c_groupRefIn
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
while c_currentRef is not None and not c_currentRef.IsEmpty():
|
|
144
|
+
c_groupObj = c_currentRef.GetObject()
|
|
145
|
+
l_pathParts.insert(0, c_groupObj.Наименование)
|
|
146
|
+
c_currentRef = c_groupObj.Родитель
|
|
147
|
+
|
|
148
|
+
s_fullPath = " > ".join(l_pathParts)
|
|
149
|
+
log_sys(f"Full group path built: '{s_fullPath}'")
|
|
150
|
+
return s_fullPath
|
|
151
|
+
except Exception as e:
|
|
152
|
+
log_sys(f"Error in getFullGroupPath: {e}", 1)
|
|
153
|
+
return ""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
# Load config from the shared data folder
|
|
10
|
+
COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
CONFIG_PATH = os.path.abspath(os.path.join(COMMON_DIR, '..', 'data', 'config.json'))
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
with open(CONFIG_PATH, 'r', encoding='utf-8-sig') as f:
|
|
15
|
+
config = json.load(f)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
config = {}
|
|
18
|
+
|
|
19
|
+
# LOGS_DIR is resolved dynamically inside the shared work/log/ directory
|
|
20
|
+
WORK_DIR = os.path.abspath(os.path.join(COMMON_DIR, '..'))
|
|
21
|
+
|
|
22
|
+
main_module = sys.modules.get('__main__')
|
|
23
|
+
if main_module and hasattr(main_module, '__file__') and main_module.__file__:
|
|
24
|
+
main_dir = os.path.dirname(os.path.abspath(main_module.__file__))
|
|
25
|
+
project_name = os.path.basename(main_dir)
|
|
26
|
+
else:
|
|
27
|
+
project_name = 'unknown'
|
|
28
|
+
|
|
29
|
+
LOGS_DIR = os.path.join(WORK_DIR, 'log', project_name)
|
|
30
|
+
|
|
31
|
+
def log_usr(message, errorflag = 0, user_id = None):
|
|
32
|
+
folder_path = os.path.join(LOGS_DIR, 'user')
|
|
33
|
+
if user_id is None:
|
|
34
|
+
user_id = "no_ID_user"
|
|
35
|
+
log_path = os.path.join(folder_path, f"{user_id}.log")
|
|
36
|
+
|
|
37
|
+
os.makedirs(folder_path, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
log_text = f"[{datetime.now().strftime('%H:%M %d.%m.%Y')}]{'[ERROR]' if errorflag else '\t'} {message}\n"
|
|
40
|
+
log(log_path, log_text)
|
|
41
|
+
|
|
42
|
+
def log_sys(message, errorFlag = 0, user_id = None):
|
|
43
|
+
folder_path = os.path.join(LOGS_DIR, 'system')
|
|
44
|
+
|
|
45
|
+
caller_frame = inspect.stack()[1]
|
|
46
|
+
caller_filename = os.path.basename(caller_frame.filename)
|
|
47
|
+
|
|
48
|
+
log_path = os.path.join(folder_path, f"{caller_filename.split('.')[0]}.log")
|
|
49
|
+
|
|
50
|
+
os.makedirs(folder_path, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
log_text = f"[{datetime.now().strftime('%H:%M %d.%m.%Y')}]{'[ERROR]' if errorFlag else '\t'} {message}\n"
|
|
53
|
+
log(log_path, log_text)
|
|
54
|
+
|
|
55
|
+
def log(file_path, log_text):
|
|
56
|
+
with open(file_path, "a", encoding="utf-8") as f:
|
|
57
|
+
f.write(log_text)
|
|
58
|
+
|
|
59
|
+
def archiveLog():
|
|
60
|
+
format = "zip"
|
|
61
|
+
source_dir = LOGS_DIR
|
|
62
|
+
|
|
63
|
+
if not os.path.exists(source_dir):
|
|
64
|
+
log_sys(f"Source directory '{source_dir}' not found. Archiving aborted.", 1)
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
filename = datetime.now().strftime('%d-%m-%Y')
|
|
68
|
+
config_path = config.get("LogDumpsPath")
|
|
69
|
+
|
|
70
|
+
if config_path and os.access(config_path, os.W_OK):
|
|
71
|
+
log_sys("Dump folder path from config is valid")
|
|
72
|
+
base_dump_path = Path(config_path)
|
|
73
|
+
else:
|
|
74
|
+
log_sys("Dump folder path from config is not valid. Dump path set to default")
|
|
75
|
+
base_dump_path = (Path(LOGS_DIR).parent.parent / "logDumps").resolve()
|
|
76
|
+
|
|
77
|
+
base_dump_path.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
full_archive_path = base_dump_path / filename
|
|
80
|
+
|
|
81
|
+
log_sys(f"Backup path generated: {full_archive_path}.{format}")
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
shutil.make_archive(str(full_archive_path), format, source_dir)
|
|
85
|
+
log_sys(f"Log archive created successfully: {filename}.{format}")
|
|
86
|
+
return 1
|
|
87
|
+
except Exception as e:
|
|
88
|
+
log_sys(f"Error during archiving logs: {e}", 1)
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
def clearLog():
|
|
92
|
+
log_path = LOGS_DIR
|
|
93
|
+
try:
|
|
94
|
+
shutil.rmtree(log_path)
|
|
95
|
+
log_sys(f"Logs cleared")
|
|
96
|
+
return 1
|
|
97
|
+
except Exception as e:
|
|
98
|
+
log_sys(f"Error during clearing logs: {e}", 1)
|
|
99
|
+
return 0
|