alegra-e-provider 0.0.1__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.
- alegra_e_provider-0.0.1/PKG-INFO +13 -0
- alegra_e_provider-0.0.1/README.md +171 -0
- alegra_e_provider-0.0.1/alegra/__init__.py +0 -0
- alegra_e_provider-0.0.1/alegra/client.py +141 -0
- alegra_e_provider-0.0.1/alegra/config.py +14 -0
- alegra_e_provider-0.0.1/alegra/models/__init__.py +0 -0
- alegra_e_provider-0.0.1/alegra/models/address.py +8 -0
- alegra_e_provider-0.0.1/alegra/models/company.py +105 -0
- alegra_e_provider-0.0.1/alegra/models/customer.py +17 -0
- alegra_e_provider-0.0.1/alegra/models/dian.py +6 -0
- alegra_e_provider-0.0.1/alegra/models/invoice.py +108 -0
- alegra_e_provider-0.0.1/alegra/models/payroll.py +8 -0
- alegra_e_provider-0.0.1/alegra/models/phone_number.py +12 -0
- alegra_e_provider-0.0.1/alegra/models/test_set.py +8 -0
- alegra_e_provider-0.0.1/alegra/resources/__init__.py +0 -0
- alegra_e_provider-0.0.1/alegra/resources/base.py +133 -0
- alegra_e_provider-0.0.1/alegra/resources/factory.py +36 -0
- alegra_e_provider-0.0.1/alegra_e_provider.egg-info/PKG-INFO +13 -0
- alegra_e_provider-0.0.1/alegra_e_provider.egg-info/SOURCES.txt +24 -0
- alegra_e_provider-0.0.1/alegra_e_provider.egg-info/dependency_links.txt +1 -0
- alegra_e_provider-0.0.1/alegra_e_provider.egg-info/requires.txt +3 -0
- alegra_e_provider-0.0.1/alegra_e_provider.egg-info/top_level.txt +1 -0
- alegra_e_provider-0.0.1/setup.cfg +4 -0
- alegra_e_provider-0.0.1/setup.py +16 -0
- alegra_e_provider-0.0.1/tests/test_company.py +57 -0
- alegra_e_provider-0.0.1/tests/test_invoice.py +95 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: alegra-e-provider
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Alegra E-Provider, Python Wrapper for Alegra Electronic Provider API
|
|
5
|
+
Author: Luis Martinez
|
|
6
|
+
Requires-Python: >=3.6
|
|
7
|
+
Requires-Dist: pydantic[email]==2.8.2
|
|
8
|
+
Requires-Dist: requests==2.32.3
|
|
9
|
+
Requires-Dist: httpx==0.27.2
|
|
10
|
+
Dynamic: author
|
|
11
|
+
Dynamic: requires-dist
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
Dynamic: summary
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Alegra API Client
|
|
2
|
+
|
|
3
|
+
This project provides an API client for the Alegra platform using the `pydantic` and `requests` libraries. The modular structure allows for flexible and extensible resource management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
1. Clone the repository.
|
|
8
|
+
2. Install the required dependencies:
|
|
9
|
+
```sh
|
|
10
|
+
pip install -r requirements.txt
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Create a configuration object to handle API key and environment settings.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from config import ApiConfig
|
|
19
|
+
|
|
20
|
+
config = ApiConfig(api_key='your_api_key', environment='sandbox')
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Initialize the API client with the configuration:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from client import ApiClient
|
|
29
|
+
|
|
30
|
+
client = ApiClient(config)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Company Operations
|
|
34
|
+
|
|
35
|
+
#### Create a Company
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from models.company import Company, NotificationByEmail
|
|
39
|
+
|
|
40
|
+
company_data = Company(
|
|
41
|
+
useAlegraCertificate=True,
|
|
42
|
+
notificationByEmail=NotificationByEmail(enabled=True)
|
|
43
|
+
)
|
|
44
|
+
new_company = client.companies.create(company_data)
|
|
45
|
+
print(new_company.id, new_company.name)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Get a Company
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
company = client.companies.get("company_id")
|
|
52
|
+
print(company.id, company.name)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Update a Company
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
updated_data = Company(name="Updated Company Name")
|
|
59
|
+
updated_company = client.companies.update("company_id", updated_data)
|
|
60
|
+
print(updated_company.name)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### List Companies
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
companies = client.companies.list()
|
|
67
|
+
for company in companies:
|
|
68
|
+
print(company.id, company.name)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Payroll Operations
|
|
72
|
+
|
|
73
|
+
#### Create a Payroll
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from models.payroll import Payroll
|
|
77
|
+
|
|
78
|
+
payroll_data = Payroll(
|
|
79
|
+
prefix="NE",
|
|
80
|
+
number=1,
|
|
81
|
+
governmentData={"key": "value"}
|
|
82
|
+
)
|
|
83
|
+
new_payroll = client.payrolls.create(payroll_data)
|
|
84
|
+
print(new_payroll.id, new_payroll.prefix)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Get a Payroll
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
payroll = client.payrolls.get("payroll_id")
|
|
91
|
+
print(payroll.id, payroll.prefix)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### Update a Payroll
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
updated_data = Payroll(prefix="NE", number=2)
|
|
98
|
+
updated_payroll = client.payrolls.update("payroll_id", updated_data)
|
|
99
|
+
print(updated_payroll.number)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### List Payrolls
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
payrolls = client.payrolls.list()
|
|
106
|
+
for payroll in payrolls:
|
|
107
|
+
print(payroll.id, payroll.prefix)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Replace a Payroll
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
replacement_data = Payroll(prefix="NE", number=2, governmentData={"key": "new_value"})
|
|
114
|
+
replaced_payroll = client.payrolls.perform_subaction("payroll_id", "replace", replacement_data)
|
|
115
|
+
print(replaced_payroll.id, replaced_payroll.number)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Cancel a Payroll
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
cancel_response = client.payrolls.perform_subaction("payroll_id", "cancel")
|
|
122
|
+
print(cancel_response)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### DIAN Operations
|
|
126
|
+
|
|
127
|
+
#### List DIAN Resources
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
dian_resources = client.dian.list()
|
|
131
|
+
for resource in dian_resources:
|
|
132
|
+
print(resource.id, resource.name)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Test Set Operations
|
|
136
|
+
|
|
137
|
+
#### Create a Test Set
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from models.test_set import TestSet
|
|
141
|
+
|
|
142
|
+
test_set_data = TestSet(
|
|
143
|
+
type="test",
|
|
144
|
+
governmentId="gov123",
|
|
145
|
+
idCompany="company_id"
|
|
146
|
+
)
|
|
147
|
+
new_test_set = client.test_sets.create(test_set_data)
|
|
148
|
+
print(new_test_set.id)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Get a Test Set
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
test_set = client.test_sets.get("test_set_id")
|
|
155
|
+
print(test_set.id, test_set.type)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Error Handling
|
|
159
|
+
|
|
160
|
+
The client raises exceptions for HTTP errors, which can be caught and handled appropriately.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
try:
|
|
164
|
+
new_company = client.companies.create(company_data)
|
|
165
|
+
except requests.exceptions.RequestException as e:
|
|
166
|
+
print(f"An error occurred: {e}")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
This project is licensed under the MIT License.
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import httpx
|
|
3
|
+
|
|
4
|
+
from alegra.config import ApiConfig
|
|
5
|
+
from alegra.models.company import Company
|
|
6
|
+
from alegra.models.dian import DianResource
|
|
7
|
+
from alegra.models.invoice import Invoice, InvoiceResponse, FileResponse
|
|
8
|
+
from alegra.models.payroll import Payroll
|
|
9
|
+
from alegra.models.test_set import TestSet
|
|
10
|
+
from alegra.resources.factory import ResourceFactory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiClient:
|
|
14
|
+
def __init__(self, config: ApiConfig, async_mode=False):
|
|
15
|
+
self.config = config
|
|
16
|
+
self.base_url = self.config.get_base_url()
|
|
17
|
+
self.async_mode = async_mode
|
|
18
|
+
self._initialize_resources()
|
|
19
|
+
|
|
20
|
+
async def _async_request(self, method, endpoint, **kwargs):
|
|
21
|
+
url = f"{self.base_url}/{endpoint}"
|
|
22
|
+
async with httpx.AsyncClient(
|
|
23
|
+
headers={"Authorization": f"Bearer {self.config.api_key}"}, timeout=30.0
|
|
24
|
+
) as client:
|
|
25
|
+
response = await client.request(method, url, **kwargs)
|
|
26
|
+
return response.json()
|
|
27
|
+
|
|
28
|
+
def _sync_request(self, method, endpoint, **kwargs):
|
|
29
|
+
url = f"{self.base_url}/{endpoint}"
|
|
30
|
+
with requests.Session() as session:
|
|
31
|
+
session.headers.update({"Authorization": f"Bearer {self.config.api_key}"})
|
|
32
|
+
response = session.request(method, url, **kwargs)
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
def _request(self, method, endpoint, **kwargs):
|
|
36
|
+
if self.async_mode:
|
|
37
|
+
return self._async_request(method, endpoint, **kwargs)
|
|
38
|
+
else:
|
|
39
|
+
return self._sync_request(method, endpoint, **kwargs)
|
|
40
|
+
|
|
41
|
+
def _initialize_resources(self):
|
|
42
|
+
self.company = ResourceFactory(
|
|
43
|
+
self,
|
|
44
|
+
"company",
|
|
45
|
+
self._request,
|
|
46
|
+
{
|
|
47
|
+
"get": {
|
|
48
|
+
"model": Company,
|
|
49
|
+
"response_model": Company,
|
|
50
|
+
"response_key": "company",
|
|
51
|
+
},
|
|
52
|
+
"update": {
|
|
53
|
+
"model": Company,
|
|
54
|
+
"response_model": Company,
|
|
55
|
+
"response_key": "company",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
self.companies = ResourceFactory(
|
|
60
|
+
self,
|
|
61
|
+
"companies",
|
|
62
|
+
self._request,
|
|
63
|
+
{
|
|
64
|
+
"create": {
|
|
65
|
+
"model": Company,
|
|
66
|
+
"response_model": Company,
|
|
67
|
+
"response_key": "company",
|
|
68
|
+
},
|
|
69
|
+
"get": {
|
|
70
|
+
"model": Company,
|
|
71
|
+
"response_model": Company,
|
|
72
|
+
"response_key": "company",
|
|
73
|
+
},
|
|
74
|
+
"update": {
|
|
75
|
+
"model": Company,
|
|
76
|
+
"response_model": Company,
|
|
77
|
+
"response_key": "company",
|
|
78
|
+
},
|
|
79
|
+
"list": {
|
|
80
|
+
"model": Company,
|
|
81
|
+
"response_model": Company,
|
|
82
|
+
"response_key": "companies",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
self.payrolls = ResourceFactory(
|
|
87
|
+
self,
|
|
88
|
+
"payrolls",
|
|
89
|
+
self._request,
|
|
90
|
+
{
|
|
91
|
+
"create": {"model": Payroll, "response_key": "payroll"},
|
|
92
|
+
"get": {"model": Payroll, "response_key": "payroll"},
|
|
93
|
+
"update": {"model": Payroll, "response_key": "payroll"},
|
|
94
|
+
"list": {"model": Payroll, "response_key": "payrolls"},
|
|
95
|
+
"perform__replace": {"model": Payroll, "response_key": "payroll"},
|
|
96
|
+
"perform__cancel": {"model": Payroll, "response_key": "payroll"},
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
self.dian = ResourceFactory(
|
|
100
|
+
self,
|
|
101
|
+
"dian",
|
|
102
|
+
self._request,
|
|
103
|
+
{"list": {"model": DianResource, "response_key": "dian"}},
|
|
104
|
+
)
|
|
105
|
+
self.test_sets = ResourceFactory(
|
|
106
|
+
self,
|
|
107
|
+
"test-sets",
|
|
108
|
+
self._request,
|
|
109
|
+
{
|
|
110
|
+
"create": {"model": TestSet, "response_key": "test_set"},
|
|
111
|
+
"get": {"model": TestSet, "response_key": "test_set"},
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
self.invoices = ResourceFactory(
|
|
115
|
+
self,
|
|
116
|
+
"invoices",
|
|
117
|
+
self._request,
|
|
118
|
+
{
|
|
119
|
+
"create": {
|
|
120
|
+
"model": Invoice,
|
|
121
|
+
"response_model": InvoiceResponse,
|
|
122
|
+
"response_key": "invoice",
|
|
123
|
+
},
|
|
124
|
+
"get": {
|
|
125
|
+
"model": Invoice,
|
|
126
|
+
"response_model": InvoiceResponse,
|
|
127
|
+
"response_key": "invoice",
|
|
128
|
+
},
|
|
129
|
+
"perform__file_xml": {
|
|
130
|
+
"model": FileResponse,
|
|
131
|
+
"endpoint_suffix": "files/XML",
|
|
132
|
+
"response_model": FileResponse,
|
|
133
|
+
"response_key": "file",
|
|
134
|
+
},
|
|
135
|
+
"list": {
|
|
136
|
+
"model": InvoiceResponse,
|
|
137
|
+
"response_model": InvoiceResponse,
|
|
138
|
+
"response_key": "invoices",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApiConfig(BaseModel):
|
|
5
|
+
api_key: str
|
|
6
|
+
environment: str
|
|
7
|
+
|
|
8
|
+
def get_base_url(self):
|
|
9
|
+
if self.environment == 'sandbox':
|
|
10
|
+
return 'https://sandbox-api.alegra.com/e-provider/col/v1'
|
|
11
|
+
elif self.environment == 'production':
|
|
12
|
+
return 'https://api.alegra.com/e-provider/col/v1'
|
|
13
|
+
else:
|
|
14
|
+
raise ValueError("Invalid environment. Choose 'sandbox' or 'production'.")
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, EmailStr, field_validator, model_validator
|
|
5
|
+
|
|
6
|
+
from .address import Address
|
|
7
|
+
|
|
8
|
+
IDENTIFICATION_TYPES = [
|
|
9
|
+
"11",
|
|
10
|
+
"12",
|
|
11
|
+
"13",
|
|
12
|
+
"21",
|
|
13
|
+
"22",
|
|
14
|
+
"31",
|
|
15
|
+
"41",
|
|
16
|
+
"42",
|
|
17
|
+
"47",
|
|
18
|
+
"48",
|
|
19
|
+
"50",
|
|
20
|
+
"91",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Webhook(BaseModel):
|
|
25
|
+
url: str = ""
|
|
26
|
+
headers: Dict[str, str] = {}
|
|
27
|
+
status: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Webhooks(BaseModel):
|
|
31
|
+
general: Dict[str, Webhook] = {}
|
|
32
|
+
payrolls: Dict[str, Webhook] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Certificate(BaseModel):
|
|
36
|
+
name: str = ""
|
|
37
|
+
extension: str = ""
|
|
38
|
+
content: str = ""
|
|
39
|
+
password: str = ""
|
|
40
|
+
issuerName: str = ""
|
|
41
|
+
startDate: str = ""
|
|
42
|
+
endDate: str = ""
|
|
43
|
+
serialNumber: str = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GovernmentStatus(BaseModel):
|
|
47
|
+
payrolls: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NotificationByEmail(BaseModel):
|
|
51
|
+
enabled: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Company(BaseModel):
|
|
55
|
+
id: Optional[str] = None
|
|
56
|
+
name: str
|
|
57
|
+
tradeName: Optional[str] = None
|
|
58
|
+
identification: str
|
|
59
|
+
dv: str
|
|
60
|
+
useAlegraCertificate: bool
|
|
61
|
+
organizationType: int
|
|
62
|
+
identificationType: str
|
|
63
|
+
regimeCode: str = ""
|
|
64
|
+
email: EmailStr
|
|
65
|
+
phone: Optional[str] = ""
|
|
66
|
+
address: Address
|
|
67
|
+
governmentStatus: Optional[GovernmentStatus] = None
|
|
68
|
+
certificate: Optional[Certificate] = None
|
|
69
|
+
webhooks: Optional[Webhooks] = None
|
|
70
|
+
notificationByEmail: Optional[NotificationByEmail] = None
|
|
71
|
+
|
|
72
|
+
@field_validator("dv")
|
|
73
|
+
def validate_dv(cls, v):
|
|
74
|
+
if not re.match(r"^([0-9]{1})$", v):
|
|
75
|
+
raise ValueError("dv must be a single digit")
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
@field_validator("identificationType")
|
|
79
|
+
def validate_identificationType(cls, v):
|
|
80
|
+
if len(v) > 2:
|
|
81
|
+
raise ValueError("identificationType must have a maximum length of 2")
|
|
82
|
+
if v not in IDENTIFICATION_TYPES:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"identificationType must be one of the allowed values: {', '.join(IDENTIFICATION_TYPES)}"
|
|
85
|
+
)
|
|
86
|
+
return v
|
|
87
|
+
|
|
88
|
+
@field_validator("organizationType")
|
|
89
|
+
def validate_organizationType(cls, v):
|
|
90
|
+
if v not in [1, 2]:
|
|
91
|
+
raise ValueError("organizationType must be 1 or 2")
|
|
92
|
+
return v
|
|
93
|
+
|
|
94
|
+
@model_validator(mode="before")
|
|
95
|
+
def check_certificate(cls, values):
|
|
96
|
+
use_alegra_certificate = values.get("useAlegraCertificate")
|
|
97
|
+
if use_alegra_certificate:
|
|
98
|
+
return values
|
|
99
|
+
|
|
100
|
+
certificate = values.get("certificate")
|
|
101
|
+
if not use_alegra_certificate and not certificate:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"certificate is required when useAlegraCertificate is False"
|
|
104
|
+
)
|
|
105
|
+
return values
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, EmailStr
|
|
4
|
+
|
|
5
|
+
from alegra.models.address import Address
|
|
6
|
+
from alegra.models.phone_number import PhoneNumber
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Customer(BaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
organizationType: int
|
|
12
|
+
identificationType: str
|
|
13
|
+
identificationNumber: Optional[str]
|
|
14
|
+
dv: Optional[str]
|
|
15
|
+
email: Optional[EmailStr] = ""
|
|
16
|
+
address: Optional[Address] = None
|
|
17
|
+
phone: str = ""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from alegra.models.customer import Customer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Resolution(BaseModel):
|
|
10
|
+
resolutionNumber: str
|
|
11
|
+
prefix: str
|
|
12
|
+
minNumber: int
|
|
13
|
+
maxNumber: int
|
|
14
|
+
startDate: str
|
|
15
|
+
endDate: str
|
|
16
|
+
technicalKey: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Taxes(BaseModel):
|
|
20
|
+
taxCode: str
|
|
21
|
+
taxAmount: float
|
|
22
|
+
taxPercentage: str
|
|
23
|
+
taxableAmount: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Item(BaseModel):
|
|
27
|
+
code: str
|
|
28
|
+
description: str
|
|
29
|
+
price: float
|
|
30
|
+
quantity: int
|
|
31
|
+
unitCode: str
|
|
32
|
+
note: Optional[str] = ""
|
|
33
|
+
subtotal: float
|
|
34
|
+
taxAmount: float
|
|
35
|
+
total: float
|
|
36
|
+
discount: float
|
|
37
|
+
discountAmount: float
|
|
38
|
+
taxes: List[Taxes] = []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Payment(BaseModel):
|
|
42
|
+
paymentForm: str
|
|
43
|
+
paymentMethod: str
|
|
44
|
+
paymentDueDate: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DiscountOrCharge(BaseModel):
|
|
48
|
+
isCharge: bool
|
|
49
|
+
reason: str
|
|
50
|
+
percentageAmount: float
|
|
51
|
+
amount: float
|
|
52
|
+
baseAmount: float
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TotalAmounts(BaseModel):
|
|
56
|
+
grossTotal: float
|
|
57
|
+
taxableTotal: float
|
|
58
|
+
taxTotal: float
|
|
59
|
+
discountTotal: float
|
|
60
|
+
chargeTotal: float
|
|
61
|
+
advanceTotal: float
|
|
62
|
+
payableTotal: float
|
|
63
|
+
currencyCode: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CompanyID(BaseModel):
|
|
67
|
+
id: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Invoice(BaseModel):
|
|
71
|
+
documentType: str
|
|
72
|
+
resolution: Resolution
|
|
73
|
+
company: CompanyID
|
|
74
|
+
customer: Customer
|
|
75
|
+
number: int
|
|
76
|
+
note: Optional[str]
|
|
77
|
+
items: List[Item]
|
|
78
|
+
payments: List[Payment]
|
|
79
|
+
discountsAndCharges: List[DiscountOrCharge] = []
|
|
80
|
+
totalAmounts: TotalAmounts
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class GovernmentResponse(BaseModel):
|
|
84
|
+
code: str
|
|
85
|
+
message: str
|
|
86
|
+
errorMessages: List[str]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class InvoiceResponse(BaseModel):
|
|
90
|
+
id: str
|
|
91
|
+
companyIdentification: str
|
|
92
|
+
customerIdentification: str
|
|
93
|
+
type: str
|
|
94
|
+
cufe: str
|
|
95
|
+
date: datetime
|
|
96
|
+
prefix: str
|
|
97
|
+
number: int
|
|
98
|
+
fullNumber: str
|
|
99
|
+
status: str
|
|
100
|
+
legalStatus: str
|
|
101
|
+
governmentResponse: GovernmentResponse
|
|
102
|
+
xmlFileName: str
|
|
103
|
+
zipFileName: str
|
|
104
|
+
qrCodeContent: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class FileResponse(BaseModel):
|
|
108
|
+
content: str
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PhoneNumber(BaseModel):
|
|
7
|
+
country_code: int
|
|
8
|
+
national_number: int
|
|
9
|
+
extension: Optional[str] = None
|
|
10
|
+
italian_leading_zero: Optional[bool] = None
|
|
11
|
+
number_of_leading_zeros: Optional[int] = None
|
|
12
|
+
preferred_domestic_carrier_code: Optional[int] = None
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Callable, Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApiResource:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
client,
|
|
10
|
+
endpoint: str,
|
|
11
|
+
request_method: Callable,
|
|
12
|
+
actions_config: Dict[str, Dict[str, str]],
|
|
13
|
+
):
|
|
14
|
+
self.client = client
|
|
15
|
+
self.endpoint = endpoint
|
|
16
|
+
self.request_method = request_method
|
|
17
|
+
self.actions_config = actions_config
|
|
18
|
+
|
|
19
|
+
def _is_action_allowed(self, action: str):
|
|
20
|
+
return action in self.actions_config
|
|
21
|
+
|
|
22
|
+
def _parse_response(self, response, action: str):
|
|
23
|
+
response_key = self.actions_config[action].get("response_key")
|
|
24
|
+
if response_key:
|
|
25
|
+
if not response_key in response:
|
|
26
|
+
if response.get("message"):
|
|
27
|
+
raise ValueError(response.get("message"))
|
|
28
|
+
if response.get("errors"):
|
|
29
|
+
raise ValueError(response.get("errors"))
|
|
30
|
+
raise ValueError(f"Response key '{response_key}' not found in response")
|
|
31
|
+
response_data = response.get(response_key, response)
|
|
32
|
+
else:
|
|
33
|
+
response_data = response
|
|
34
|
+
model = self.actions_config[action]["response_model"]
|
|
35
|
+
return model.model_validate(response_data)
|
|
36
|
+
|
|
37
|
+
def _prepare_data(self, data: BaseModel):
|
|
38
|
+
data = data.model_dump()
|
|
39
|
+
if "customer" in data:
|
|
40
|
+
customer_data = data["customer"]
|
|
41
|
+
if customer_data["dv"] is None:
|
|
42
|
+
del customer_data["dv"]
|
|
43
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
44
|
+
|
|
45
|
+
def get(self, resource_id: str):
|
|
46
|
+
action = "get"
|
|
47
|
+
if not self._is_action_allowed(action):
|
|
48
|
+
raise NotImplementedError(
|
|
49
|
+
f"The action 'get' is not allowed for {self.endpoint}"
|
|
50
|
+
)
|
|
51
|
+
endpoint = f"{self.endpoint}/{resource_id}"
|
|
52
|
+
response = self.request_method("GET", endpoint)
|
|
53
|
+
return self._parse_response(response, action)
|
|
54
|
+
|
|
55
|
+
def create(self, data: BaseModel):
|
|
56
|
+
if self.client.async_mode:
|
|
57
|
+
return self.create_async(data)
|
|
58
|
+
else:
|
|
59
|
+
action = "create"
|
|
60
|
+
if not self._is_action_allowed(action):
|
|
61
|
+
raise NotImplementedError(
|
|
62
|
+
f"The action 'create' is not allowed for {self.endpoint}"
|
|
63
|
+
)
|
|
64
|
+
response = self.request_method(
|
|
65
|
+
"POST", self.endpoint, json=self._prepare_data(data)
|
|
66
|
+
)
|
|
67
|
+
return self._parse_response(response, action)
|
|
68
|
+
|
|
69
|
+
async def create_async(self, data: BaseModel):
|
|
70
|
+
action = "create"
|
|
71
|
+
if not self._is_action_allowed(action):
|
|
72
|
+
raise NotImplementedError(
|
|
73
|
+
f"The action 'create' is not allowed for {self.endpoint}"
|
|
74
|
+
)
|
|
75
|
+
response = await self.request_method(
|
|
76
|
+
"POST", self.endpoint, json=self._prepare_data(data)
|
|
77
|
+
)
|
|
78
|
+
return self._parse_response(response, action)
|
|
79
|
+
|
|
80
|
+
def update(self, resource_id: str, data: BaseModel):
|
|
81
|
+
action = "update"
|
|
82
|
+
if not self._is_action_allowed(action):
|
|
83
|
+
raise NotImplementedError(
|
|
84
|
+
f"The action 'update' is not allowed for {self.endpoint}"
|
|
85
|
+
)
|
|
86
|
+
endpoint = f"{self.endpoint}/{resource_id}"
|
|
87
|
+
response = self.request_method("PATCH", endpoint, json=self._prepare_data(data))
|
|
88
|
+
return self._parse_response(response, action)
|
|
89
|
+
|
|
90
|
+
def delete(self, resource_id: str):
|
|
91
|
+
action = "delete"
|
|
92
|
+
if not self._is_action_allowed(action):
|
|
93
|
+
raise NotImplementedError(
|
|
94
|
+
f"The action 'delete' is not allowed for {self.endpoint}"
|
|
95
|
+
)
|
|
96
|
+
endpoint = f"{self.endpoint}/{resource_id}"
|
|
97
|
+
response = self.request_method("DELETE", endpoint)
|
|
98
|
+
return response.status_code == 204
|
|
99
|
+
|
|
100
|
+
def list(self, params=None):
|
|
101
|
+
action = "list"
|
|
102
|
+
if not self._is_action_allowed(action):
|
|
103
|
+
raise NotImplementedError(
|
|
104
|
+
f"The action 'list' is not allowed for {self.endpoint}"
|
|
105
|
+
)
|
|
106
|
+
response = self.request_method("GET", self.endpoint, params=params)
|
|
107
|
+
return [
|
|
108
|
+
self.actions_config[action]["model"].model_validate(item)
|
|
109
|
+
for item in response.get(
|
|
110
|
+
self.actions_config[action].get("response_key", [])
|
|
111
|
+
)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
def perform_subaction(
|
|
115
|
+
self, resource_id: str, subaction: str, data: BaseModel = None
|
|
116
|
+
):
|
|
117
|
+
action = f"perform__{subaction}"
|
|
118
|
+
if not self._is_action_allowed(action):
|
|
119
|
+
raise NotImplementedError(
|
|
120
|
+
f"The subaction '{subaction}' is not allowed for {self.endpoint}"
|
|
121
|
+
)
|
|
122
|
+
endpoint_suffix = self.actions_config[action].get("endpoint_suffix", subaction)
|
|
123
|
+
endpoint = f"{self.endpoint}/{resource_id}/{endpoint_suffix}"
|
|
124
|
+
response = self.request_method(
|
|
125
|
+
self._request_method_for_subaction(subaction),
|
|
126
|
+
endpoint,
|
|
127
|
+
json=self._prepare_data(data) if data else {},
|
|
128
|
+
)
|
|
129
|
+
return self._parse_response(response, action)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def _request_method_for_subaction(subaction: str):
|
|
133
|
+
return "POST" if subaction in ["replace", "cancel"] else "GET"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Callable, Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from .base import ApiResource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResourceFactory:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
client,
|
|
12
|
+
endpoint: str,
|
|
13
|
+
request_method: Callable,
|
|
14
|
+
actions_config: Dict[str, Dict[str, str]],
|
|
15
|
+
):
|
|
16
|
+
self.resource = ApiResource(client, endpoint, request_method, actions_config)
|
|
17
|
+
|
|
18
|
+
def get(self, resource_id):
|
|
19
|
+
return self.resource.get(resource_id)
|
|
20
|
+
|
|
21
|
+
def create(self, data):
|
|
22
|
+
return self.resource.create(data)
|
|
23
|
+
|
|
24
|
+
def update(self, resource_id, data):
|
|
25
|
+
return self.resource.update(resource_id, data)
|
|
26
|
+
|
|
27
|
+
def delete(self, resource_id):
|
|
28
|
+
return self.resource.delete(resource_id)
|
|
29
|
+
|
|
30
|
+
def list(self, params=None):
|
|
31
|
+
return self.resource.list(params)
|
|
32
|
+
|
|
33
|
+
def perform_subaction(
|
|
34
|
+
self, resource_id: str, subaction: str, data: BaseModel = None
|
|
35
|
+
):
|
|
36
|
+
return self.resource.perform_subaction(resource_id, subaction, data)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: alegra-e-provider
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Alegra E-Provider, Python Wrapper for Alegra Electronic Provider API
|
|
5
|
+
Author: Luis Martinez
|
|
6
|
+
Requires-Python: >=3.6
|
|
7
|
+
Requires-Dist: pydantic[email]==2.8.2
|
|
8
|
+
Requires-Dist: requests==2.32.3
|
|
9
|
+
Requires-Dist: httpx==0.27.2
|
|
10
|
+
Dynamic: author
|
|
11
|
+
Dynamic: requires-dist
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
Dynamic: summary
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
alegra/__init__.py
|
|
4
|
+
alegra/client.py
|
|
5
|
+
alegra/config.py
|
|
6
|
+
alegra/models/__init__.py
|
|
7
|
+
alegra/models/address.py
|
|
8
|
+
alegra/models/company.py
|
|
9
|
+
alegra/models/customer.py
|
|
10
|
+
alegra/models/dian.py
|
|
11
|
+
alegra/models/invoice.py
|
|
12
|
+
alegra/models/payroll.py
|
|
13
|
+
alegra/models/phone_number.py
|
|
14
|
+
alegra/models/test_set.py
|
|
15
|
+
alegra/resources/__init__.py
|
|
16
|
+
alegra/resources/base.py
|
|
17
|
+
alegra/resources/factory.py
|
|
18
|
+
alegra_e_provider.egg-info/PKG-INFO
|
|
19
|
+
alegra_e_provider.egg-info/SOURCES.txt
|
|
20
|
+
alegra_e_provider.egg-info/dependency_links.txt
|
|
21
|
+
alegra_e_provider.egg-info/requires.txt
|
|
22
|
+
alegra_e_provider.egg-info/top_level.txt
|
|
23
|
+
tests/test_company.py
|
|
24
|
+
tests/test_invoice.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
alegra
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="alegra-e-provider",
|
|
5
|
+
packages=find_packages(include=["alegra", "alegra.*"]),
|
|
6
|
+
version="0.0.1",
|
|
7
|
+
description="Alegra E-Provider, Python Wrapper for Alegra Electronic Provider API",
|
|
8
|
+
author="Luis Martinez",
|
|
9
|
+
install_requires=[
|
|
10
|
+
"pydantic[email]==2.8.2",
|
|
11
|
+
"requests==2.32.3",
|
|
12
|
+
"httpx==0.27.2",
|
|
13
|
+
],
|
|
14
|
+
test_suite="tests",
|
|
15
|
+
python_requires=">=3.6",
|
|
16
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
|
|
4
|
+
from alegra.client import ApiClient
|
|
5
|
+
from alegra.config import ApiConfig
|
|
6
|
+
from alegra.models.company import (
|
|
7
|
+
Address,
|
|
8
|
+
Certificate,
|
|
9
|
+
Company,
|
|
10
|
+
GovernmentStatus,
|
|
11
|
+
NotificationByEmail,
|
|
12
|
+
Webhook,
|
|
13
|
+
Webhooks,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestCompanyResource(unittest.TestCase):
|
|
18
|
+
def setUp(self):
|
|
19
|
+
self.config = ApiConfig(
|
|
20
|
+
api_key="ce5PgRbegtMgcwt9pPnPYNpCNuy0Nxmm5ohKZHSU", environment="sandbox"
|
|
21
|
+
)
|
|
22
|
+
self.client = ApiClient(self.config)
|
|
23
|
+
|
|
24
|
+
def test_create_company(self):
|
|
25
|
+
company_data = Company(
|
|
26
|
+
name="Soluciones Alegra S.A.S",
|
|
27
|
+
identification="111111111",
|
|
28
|
+
dv="2",
|
|
29
|
+
useAlegraCertificate=True,
|
|
30
|
+
governmentStatus=GovernmentStatus(payrolls="AUTHORIZED"),
|
|
31
|
+
organizationType=1,
|
|
32
|
+
identificationType="31",
|
|
33
|
+
regimeCode="R-99-PN",
|
|
34
|
+
email="email@email.com",
|
|
35
|
+
phone="1234567890",
|
|
36
|
+
address=Address(
|
|
37
|
+
address="Cra. 13 #12-12 Edificio A & A",
|
|
38
|
+
city="11001",
|
|
39
|
+
department="11",
|
|
40
|
+
country="CO",
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
company = self.client.companies.create(company_data)
|
|
45
|
+
|
|
46
|
+
self.assertEqual(company.name, "Soluciones Alegra S.A.S")
|
|
47
|
+
self.assertEqual(company.identification, "111111111")
|
|
48
|
+
self.assertEqual(company.dv, "2")
|
|
49
|
+
self.assertTrue(company.useAlegraCertificate)
|
|
50
|
+
self.assertEqual(company.organizationType, 1)
|
|
51
|
+
self.assertEqual(company.identificationType, "31")
|
|
52
|
+
self.assertEqual(company.regimeCode, "R-99-PN")
|
|
53
|
+
self.assertEqual(company.email, "email@email.com")
|
|
54
|
+
self.assertEqual(company.address.address, "Cra. 13 #12-12 Edificio A & A")
|
|
55
|
+
self.assertEqual(company.address.city, "11001")
|
|
56
|
+
self.assertEqual(company.address.department, "11")
|
|
57
|
+
self.assertEqual(company.address.country, "CO")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from alegra.client import ApiClient
|
|
4
|
+
from alegra.config import ApiConfig
|
|
5
|
+
from alegra.models.company import Address, Company, GovernmentStatus
|
|
6
|
+
from alegra.models.invoice import Invoice
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCompanyResource(unittest.TestCase):
|
|
10
|
+
def setUp(self):
|
|
11
|
+
self.config = ApiConfig(
|
|
12
|
+
api_key="ce5PgRbegtMgcwt9pPnPYNpCNuy0Nxmm5ohKZHSU", environment="sandbox"
|
|
13
|
+
)
|
|
14
|
+
self.client = ApiClient(self.config)
|
|
15
|
+
|
|
16
|
+
def test_create_invoice(self):
|
|
17
|
+
|
|
18
|
+
invoice_data = Invoice(
|
|
19
|
+
**{
|
|
20
|
+
"documentType": "01",
|
|
21
|
+
"resolution": {
|
|
22
|
+
"resolutionNumber": "18760000001",
|
|
23
|
+
"prefix": "SETP",
|
|
24
|
+
"minNumber": 990000000,
|
|
25
|
+
"maxNumber": 995000000,
|
|
26
|
+
"startDate": "2019-01-19",
|
|
27
|
+
"endDate": "2030-01-19",
|
|
28
|
+
"technicalKey": "fc8eac422eba16e22ffd8c6f94b3f40a6e38162c",
|
|
29
|
+
},
|
|
30
|
+
"company": {
|
|
31
|
+
"id": "01HTJ3D9NZA79Z6BRNHB19C6Z0",
|
|
32
|
+
},
|
|
33
|
+
"customer": {
|
|
34
|
+
"address": {
|
|
35
|
+
"address": "Cra. 13 #12-12 Edificio A & A",
|
|
36
|
+
"city": "11001",
|
|
37
|
+
"department": "11",
|
|
38
|
+
"country": "CO",
|
|
39
|
+
},
|
|
40
|
+
"name": "Customer Name",
|
|
41
|
+
"organizationType": 2,
|
|
42
|
+
"identificationType": "13",
|
|
43
|
+
"identificationNumber": "222222222222",
|
|
44
|
+
"dv": "2",
|
|
45
|
+
"email": "email@email.com",
|
|
46
|
+
"phone": "3223334455",
|
|
47
|
+
},
|
|
48
|
+
"totalAmounts": {
|
|
49
|
+
"grossTotal": 10000,
|
|
50
|
+
"taxableTotal": 10000,
|
|
51
|
+
"taxTotal": 1900,
|
|
52
|
+
"discountTotal": 0,
|
|
53
|
+
"chargeTotal": 0,
|
|
54
|
+
"advanceTotal": 0,
|
|
55
|
+
"payableTotal": 11900,
|
|
56
|
+
"currencyCode": "COP",
|
|
57
|
+
},
|
|
58
|
+
"number": 990015000,
|
|
59
|
+
"note": "Nota relativa al documento",
|
|
60
|
+
"items": [
|
|
61
|
+
{
|
|
62
|
+
"code": "BM3",
|
|
63
|
+
"standardCode": {"identificationId": "11001", "id": "001"},
|
|
64
|
+
"description": "Botella de M3",
|
|
65
|
+
"price": 10000,
|
|
66
|
+
"discount": 0,
|
|
67
|
+
"discountAmount": 0,
|
|
68
|
+
"quantity": 1,
|
|
69
|
+
"unitCode": "94",
|
|
70
|
+
"subtotal": 10000,
|
|
71
|
+
"taxAmount": 1900,
|
|
72
|
+
"total": 11900,
|
|
73
|
+
"taxes": [
|
|
74
|
+
{
|
|
75
|
+
"taxCode": "01",
|
|
76
|
+
"taxAmount": 1900,
|
|
77
|
+
"taxPercentage": "19.00",
|
|
78
|
+
"taxableAmount": 10000,
|
|
79
|
+
"taxBaseUnitMeasure": 1,
|
|
80
|
+
"taxPerUnitAmount": 1,
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"payments": [
|
|
86
|
+
{
|
|
87
|
+
"paymentForm": "1",
|
|
88
|
+
"paymentMethod": "10",
|
|
89
|
+
"paymentDueDate": "2021-08-30",
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
invoice = self.client.invoices.create(invoice_data)
|
|
95
|
+
self.assertIsNotNone(invoice.id)
|