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.
@@ -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,8 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Address(BaseModel):
5
+ address: str = ""
6
+ city: str = ""
7
+ department: str = ""
8
+ country: str = ""
@@ -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,6 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class DianResource(BaseModel):
5
+ id: str
6
+ name: 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,8 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Payroll(BaseModel):
5
+ id: str = None
6
+ prefix: str
7
+ number: int
8
+ governmentData: dict
@@ -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
@@ -0,0 +1,8 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class TestSet(BaseModel):
5
+ id: str
6
+ type: str
7
+ governmentId: str
8
+ idCompany: str
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,3 @@
1
+ pydantic[email]==2.8.2
2
+ requests==2.32.3
3
+ httpx==0.27.2
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)