diff --git a/.vscode/settings.json b/.vscode/settings.json index b96b7004..187aac7a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,9 +2,9 @@ "python.testing.unittestArgs": [ "-v", "-s", - "./src", + "./src/functions/test/", "-p", - "*test*.py" + "test*.py" ], "python.testing.pytestEnabled": false, "python.testing.unittestEnabled": true, diff --git a/docker-compose.yml b/docker-compose.yml index 9703914f..37b7daf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,18 +9,18 @@ services: POSTGRES_PASSWORD: password POSTGRES_USER: user POSTGRES_DB: db - ports: - - 5432:5432 volumes: - ./db_data:/var/lib/postgresql/data + ports: + - 5432:5432 + bot: build: context: ./src/bot dockerfile: Dockerfile restart: always - environment: - - API_KEY=${API_KEY} + site: build: @@ -36,6 +36,7 @@ services: volumes: - ./src/site:/app + pg-admin: image: dpage/pgadmin4 depends_on: @@ -48,6 +49,7 @@ services: links: - db:db + functions: build: context: ./src/functions diff --git a/src/functions/bd_update_sales/api_ifood.py b/src/functions/bd_update_sales/api_ifood.py index 32f1d177..d9dcd5a8 100644 --- a/src/functions/bd_update_sales/api_ifood.py +++ b/src/functions/bd_update_sales/api_ifood.py @@ -1,16 +1,188 @@ -from importer import * from dotenv import load_dotenv +from importer import ImporterApi_Interface +from retry import retry +from bd_sales import DbSalesIfood +import os, time, requests, hashlib def configure(): load_dotenv() - -class APIIfood(ImporterApi): - ifood = ImporterApi('ifood') - api_name = 'ifood' + +# STATIC VARIABLES +BASE_URL = 'https://merchant-api.ifood.com.br' + + +# Implements the "Importer" interface +# to provide all the necessary methods +# for downloading Ifood API data and saving it +# in the database. + +class ApiIfood(ImporterApi_Interface): - def __init__(self, api): + def __init__(self): configure() + self.CLIENT_ID = os.getenv('IFOOD_CLIENT_ID') + self.CLIENT_SECRET = os.getenv('IFOOD_CLIENT_SECRET') + self.api_name = 'ifood' + self.accessToken = None + + + ############################### + # CONNECT # + ############################### def connect(self): - print('Getting API KEY') \ No newline at end of file + + print(f'Getting Access Token from {self.api_name}...') + + try: + if self.accessToken != None: + return + + URL = f'{BASE_URL}/authentication/v1.0/oauth/token' + data={ + 'clientId': self.CLIENT_ID, + 'clientSecret': self.CLIENT_SECRET, + 'grantType': 'client_credentials' + } + + post = requests.post(URL, data=data) + self.accessToken = post.json()['accessToken'] + + finally: + self.configure_headers(self.accessToken) + print(f'\tAccess Token obtained!') + + + + def configure_headers(self, access_token): + auth = f'Bearer {self.accessToken}' + self.headers = {"Authorization": auth} + + + ############################### + # DOWNLOAD # + ############################### + + def download(self) -> bool: + + print(f'Downloading data from {self.api_name}...') + + # MERCHANTS + #self.merchants = self.download_merchants() + #self.merchants_details = self.download_merchants_details(merchants) + #self.merchants_hash_downloaded = hashlib.md5(str(self.merchants).encode('utf-8')).hexdigest() + + # ORDERS + orders = self.download_orders() + self.orders_details = self.download_orders_details(orders) if orders != None else None + + return True + + + def download_merchants(self): + + print('\tDownloading merchants...') + merchants = [] + URL = f'{BASE_URL}/merchant/v1.0/merchants' + post = requests.get(URL, headers=self.headers) + + return post.json() + + + def download_merchants_details(self, merchants): + + print('\tDownloading merchants details...') + URL = f'{BASE_URL}/merchant/v1.0/merchants/' + merchants_details = [] + + for merchant in merchants: + post = requests.get(URL + merchant['id'], headers=self.headers) + merchants_details.append(post.json()) + + return merchants_details + + + def download_orders(self): + + print('\tDownloading orders...') + orders = [] + URL = f'{BASE_URL}/order/v1.0/events:polling' + post = requests.get(URL, headers=self.headers) + + if post.status_code != 200: + print(f'\t\tStatus code: {post.status_code}') + return None + + return post.json() + + + def download_orders_details(self, orders): + + print('\tDownloading orders details...') + orders_details = [] + URL = f'{BASE_URL}/order/v1.0/orders/' + + for order in orders: + URL = f"{URL}{order['orderId']}" + post = requests.get(URL, headers=self.headers) + + if post.status_code == 200: + orders_details.append(post.json()) + + return orders_details + + + ############################### + # SAVE # + ############################### + + def save_db(self): + + print(f'Saving data from {self.api_name}...') + + db = DbSalesIfood() + + #if self.merchants_hash_downloaded != self.merchants_hash_saved: + #db.insert_merchants(self.merchants) + #self.merchants_hash_saved = self.merchants_hash_downloaded + + if self.orders_details != None: + db.insert_orders(self.orders_details) + + + def send_acks(): + pass + + + def send_ack(id): + pass + + + ############################### + # WATCHER # + ############################### + + @retry(delay=10, tries=1000) + def start(self): + + try: + + self.connect() + self.merchants_hash_saved = None + + while True: + if self.download(): + self.save_db() + + time.sleep(5) + + except Exception as e: + print(f'Error: {e}') + raise e + + + +def start(): + ifood = ApiIfood() + ifood.start() \ No newline at end of file diff --git a/src/functions/bd_update_sales/api_rappi.py b/src/functions/bd_update_sales/api_rappi.py new file mode 100644 index 00000000..2ae6719e --- /dev/null +++ b/src/functions/bd_update_sales/api_rappi.py @@ -0,0 +1,58 @@ +from dotenv import load_dotenv +from .importer import ImporterApi_Interface +from retry import retry +import os, time + +def configure(): + load_dotenv() + + +# STATIC VARIABLES +BASE_URL = 'https://merchant-api.ifood.com.br' +CLIENT_ID = os.getenv('IFOOD_CLIENT_ID') +CLIENT_SECRET = os.getenv('IFOOD_CLIENT_SECRET') + + +# Implements the "Importer" interface +# to provide all the necessary methods +# for downloading Rappi API data and saving it +# in the database. + +class ApiRappi(ImporterApi_Interface): + + def __init__(self): + configure() + self.api_name = 'rappi' + + + def connect(self): + print(f'Getting API KEY from {self.api_name}...') + + + def download(self) -> bool: + print(f'Downloading data from {self.api_name}...') + + + def save_db(self) -> bool: + print(f'Saving data from {self.api_name}...') + + + @retry(delay=120, tries=1000) + def start(self): + + try: + + while True: + self.connect() + self.download() + self.save_db() + time.sleep(7) + + except Exception as e: + print(f'Error: {e}') + + + +def start(): + rappi = ApiRappi() + rappi.start() \ No newline at end of file diff --git a/src/functions/bd_update_sales/apis.py b/src/functions/bd_update_sales/apis.py index 163c705e..854ab491 100644 --- a/src/functions/bd_update_sales/apis.py +++ b/src/functions/bd_update_sales/apis.py @@ -1,30 +1,63 @@ -class connectAPI: - - def __init__(self, api_name): - print(f'Starting API to {api_name}') - self.api_name = api_name +from multiprocessing import Pool, Process +import importlib, multiprocessing + + +def get_apis_list(apis_name): + + modules_name = lambda x : f'api_{x}' + modules_api_list = list(map(modules_name, apis_name)) + return modules_api_list + + + +def transform_module_into_processes(modules): + + procs = [] + + for module in modules: + + print(f'Importing module: {module}') + mod = importlib.import_module(module) + # Creating process + proc = multiprocessing.Process(target=mod.start) + procs.append(proc) - def send_email(self): - print(f'Sending email to {self.api_name}') - pass + return procs + + + +def run_processes(procs): + # Starting all processes + for proc in procs: + proc.start() - def connect(self): - print(f'Connecting to {self.api_name}') - pass + for proc in procs: + proc.join() + + + + +def send_email(): + pass + + + +def run(): + + # Gets the list of all modules(files) starting with "api_ + base list" + API_BASE_LIST = ['ifood'] + modules = get_apis_list(API_BASE_LIST) + + # Turns all modules into processes, with the "start" method to run + procs = transform_module_into_processes(modules) + + run_processes(procs) - def run(self): - - try: - print(f'Running {self.api_name}') - self.send_email() - self.connect() - print(f'Finished sucessfully {self.api_name}!') - return True - except: - print(f'Error running {self.api_name}') - return False - \ No newline at end of file + + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/src/functions/bd_update_sales/bd_sales.py b/src/functions/bd_update_sales/bd_sales.py new file mode 100644 index 00000000..441467bf --- /dev/null +++ b/src/functions/bd_update_sales/bd_sales.py @@ -0,0 +1,156 @@ +from dotenv import load_dotenv +import os, psycopg2 + + +def configure(): + load_dotenv() + + +class DbSales(): + + + def __init__(self): + configure() + self.DB_HOST = os.getenv('DATABASE_SALES_ADDRESS') + self.DB_PORT = os.getenv('DATABASE_SALES_PORT') + self.DB_USER = os.getenv('DATABASE_SALES_USER') + self.DB_PASSWORD = os.getenv('DATABASE_SALES_PASSWORD') + self.DB_NAME = 'db' + + + + def connect(self): + + try: + conecction_format = f'dbname={self.DB_NAME} user={self.DB_USER} password={self.DB_PASSWORD} host={self.DB_HOST} port={self.DB_PORT}' + self.connection = psycopg2.connect(conecction_format) + print('Connected to database!') + self.db = self.connection.cursor() + return True + + except Exception as e: + print(f"Error while connecting: {e}") + return False + + + + def execute(self, command, values): + + try: + + self.db.execute(command, values) + + try: + self.bd_return = self.db.fetchall() + except: + self.bd_return = None + + print(f'Executed: [{command}] -> Result: {self.bd_return}') + self.commit() + return self.bd_return + + except Exception as e: + print(f"Error while executing: {e}") + return None + + + + def commit(self): + + try: + self.connection.commit() + return True + + except Exception as e: + print(f"Error while commiting: {e}") + return False + + + + def disconnect(self): + self.db.close() + self.connection.close() + print('Disconnected from database!') + + + +class DbSalesIfood(): + + def insert_merchants(self, merchants): + + db = DbSales() + db.connect() + + for merchant in merchants: + + print(f'Inserting merchant: {merchant}') + + + db.disconnect() + + + + + def insert_orders(self, orders): + + self.db = DbSales() + self.db.connect() + + for order in orders: + + id_sale = self.insert_sale(order)[0] + self.insert_transaction_base(id_sale, order) + + self.db.disconnect() + + + + + def insert_sale(self, order): + + total = order['total'] + + total_products = total['subTotal'] + total_shipping = total['deliveryFee'] + total_discount = total['benefits'] + total_fees = total['additionalFees'] + order_amount = total['orderAmount'] + + pre_paid_amount = order['payments']['prepaid'] + pending_amount = order['payments']['pending'] + + db_table = 'api_transaction_sale' + + id_sale = self.db.execute(f'INSERT INTO {db_table} (total_products, total_shipping, total_discount, total_fees, order_amount, prepaid_amount, pending_amount) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id', (total_products, total_shipping, total_discount, total_fees, order_amount, pre_paid_amount, pending_amount)) + + return id_sale[0] + + + + def get_source_id(self, order): + + db_table = 'integrations_integrationtransaction' + merchant_id = order['merchant']['id'] + source_id = self.db.execute(f'SELECT id FROM {db_table} WHERE identifier = %s', (merchant_id,)) + return source_id[0][0] + + + + def insert_transaction_base(self, id_sale, order): + + db_table = 'api_transaction_transaction' + + created_at = order['createdAt'] + api_source_id = self.get_source_id(order) + api_id = order['id'] + + self.db.execute(f'INSERT INTO {db_table} (api_id, created_at, api_source_id, sale_id) VALUES (%s, %s, %s, %s)', (api_id, created_at, api_source_id, id_sale)) + + + + + def insert_transaction_client(): + pass + + def insert_transaction_delivery(): + pass \ No newline at end of file diff --git a/src/functions/bd_update_sales/importer.py b/src/functions/bd_update_sales/importer.py index ff69f838..caf12e90 100644 --- a/src/functions/bd_update_sales/importer.py +++ b/src/functions/bd_update_sales/importer.py @@ -1,29 +1,25 @@ from abc import abstractmethod, ABC - +# Interface that should be used for all APIs. class ImporterApi_Interface(ABC): @abstractmethod - def __init__(self, api): - self.api = api - print(f'Starting API to {self.api}...') + def __init__(self): + pass @abstractmethod def connect(self) -> bool: - print(f'Connecting to {self.api}...') pass @abstractmethod def download(self) -> bool: - print(f'Downloading data from {self.api}...') pass @abstractmethod def save_db(self) -> bool: - print(f'Saving data from {self.api}...') pass diff --git a/src/functions/bd_update_sales/requirements.txt b/src/functions/bd_update_sales/requirements.txt index e69de29b..ca6d0bad 100644 --- a/src/functions/bd_update_sales/requirements.txt +++ b/src/functions/bd_update_sales/requirements.txt @@ -0,0 +1,3 @@ +retry +python-dotenv +psychopg2-binary \ No newline at end of file diff --git a/src/functions/main.py b/src/functions/main.py index 9554f7d2..43e103f3 100644 --- a/src/functions/main.py +++ b/src/functions/main.py @@ -1,10 +1,10 @@ from multiprocessing import Pool from requirements_generator import install_requirements -from .bd_update_sales import * +from bd_update_sales.apis import run -install_requirements() - -#with Pool() as p: -# results = p.imap_unordered(bd_update_sales) \ No newline at end of file +if __name__ == '__main__': + print(f'Starting system at {__name__}...') + install_requirements() + run() diff --git a/src/functions/requirements.txt b/src/functions/requirements.txt index e217fb96..92b61091 100644 --- a/src/functions/requirements.txt +++ b/src/functions/requirements.txt @@ -1,9 +1,11 @@ -gspread -pandas requests +gspread openpyxl +pandas google-api-python-client google-auth-httplib2 oauth2client xlrd flask +retry +python-dotenv diff --git a/src/functions/requirements_generator.py b/src/functions/requirements_generator.py index bdd8978c..5c8cb5d1 100644 --- a/src/functions/requirements_generator.py +++ b/src/functions/requirements_generator.py @@ -4,7 +4,7 @@ try: - ACTUAL_FOLDER = os.getcwd() + "\\src\\functions" + ACTUAL_FOLDER = os.path.join(os.getcwd(), "src", "functions") ACTUAL_FOLDERS_REQUIREMENTS = os.listdir("src/functions") ACTUAL_FILE_REQUIREMENTS = os.path.join(ACTUAL_FOLDER, "requirements.txt") os.path.exists(ACTUAL_FOLDER) diff --git a/src/functions/test/test_bd_update_sales.py b/src/functions/test/test_bd_update_sales.py new file mode 100644 index 00000000..0de0b6ff --- /dev/null +++ b/src/functions/test/test_bd_update_sales.py @@ -0,0 +1,15 @@ +import unittest, sys, os + +# Configure path to import modules +current = os.path.dirname(os.path.realpath(__file__)) +parent = os.path.dirname(current) +sys.path.append(parent) + +from bd_update_sales.apis import * + + + +class TestApi(unittest.TestCase): + + def test_api_list(self): + self.assertEqual(get_apis_list(['ifood', 'rappi']), ['api_ifood', 'api_rappi']) \ No newline at end of file diff --git a/src/functions/test/test_extract.py b/src/functions/test/test_extract.py index adc5a38f..3062764a 100644 --- a/src/functions/test/test_extract.py +++ b/src/functions/test/test_extract.py @@ -21,7 +21,3 @@ def test_extract_type(self): self.assertEqual(extract_type('GETNET DEBITO ELO', 'XYZ'), 'GETNET') self.assertEqual(extract_type('CONVENIO', 'DEB AUTOMATICO'), 'DEB AUTOMATICO') self.assertEqual(extract_type('DEBITO CONVENIOS ID', '9876'), 'DEB AUTOMATICO') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/site/Dockerfile b/src/site/Dockerfile index fc35bc66..f62a1523 100644 --- a/src/site/Dockerfile +++ b/src/site/Dockerfile @@ -1,9 +1,15 @@ FROM python:3.10 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + COPY . /app WORKDIR /app RUN pip install requests -r requirements.txt + EXPOSE 8000 -CMD ["python", "manage.py", "runserver"] \ No newline at end of file +RUN chown -R $USER:$USER manage.py + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] \ No newline at end of file diff --git a/src/functions/bd_update_sales/main.py b/src/site/api_transaction/__init__.py similarity index 100% rename from src/functions/bd_update_sales/main.py rename to src/site/api_transaction/__init__.py diff --git a/src/site/api_transaction/admin.py b/src/site/api_transaction/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/src/site/api_transaction/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/site/api_transaction/apps.py b/src/site/api_transaction/apps.py new file mode 100644 index 00000000..7f359bab --- /dev/null +++ b/src/site/api_transaction/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiTransactionConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api_transaction' diff --git a/src/site/api_transaction/migrations/0001_initial.py b/src/site/api_transaction/migrations/0001_initial.py new file mode 100644 index 00000000..e90cfcd1 --- /dev/null +++ b/src/site/api_transaction/migrations/0001_initial.py @@ -0,0 +1,147 @@ +# Generated by Django 4.1.5 on 2023-01-10 01:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('street_name', models.CharField(max_length=100, null=True)), + ('street_number', models.CharField(max_length=10, null=True)), + ('neighborhood', models.CharField(max_length=100, null=True)), + ('complement', models.CharField(max_length=100, null=True)), + ('reference', models.CharField(max_length=100, null=True)), + ('postal_code', models.CharField(max_length=10, null=True)), + ('city', models.CharField(max_length=100, null=True)), + ('country', models.CharField(max_length=50, null=True)), + ('latitude', models.CharField(max_length=15, null=True)), + ('longitude', models.CharField(max_length=15, null=True)), + ], + ), + migrations.CreateModel( + name='Benefit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.FloatField()), + ('target', models.CharField(max_length=20)), + ('sponsorship_name', models.CharField(max_length=20)), + ('sponsorship_description', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Client', + fields=[ + ('name', models.CharField(max_length=100)), + ('email', models.EmailField(blank=True, max_length=254)), + ('phone_number', models.CharField(blank=True, max_length=20)), + ('document', models.CharField(max_length=14, null=True)), + ('group', models.CharField(blank=True, max_length=100)), + ('id', models.CharField(max_length=100, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='Delivery', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('mode', models.CharField(max_length=20, null=True)), + ('delivery_by', models.CharField(max_length=20, null=True)), + ('delivery_date_time', models.DateTimeField(null=True)), + ('observations', models.CharField(max_length=100, null=True)), + ], + ), + migrations.CreateModel( + name='Fee', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(max_length=100)), + ('full_description', models.CharField(max_length=100)), + ('value', models.FloatField()), + ('liabilities', models.CharField(max_length=200)), + ], + ), + migrations.CreateModel( + name='Ifood', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('display_id', models.CharField(max_length=10)), + ('order_type', models.CharField(max_length=20)), + ('order_timing', models.CharField(max_length=20)), + ('sales_channel', models.CharField(max_length=30)), + ('extra_info', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='IfoodClient', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('orders_count', models.IntegerField(null=True)), + ('segmentation', models.CharField(max_length=20, null=True)), + ], + ), + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.FloatField()), + ('currency', models.CharField(max_length=3)), + ('type', models.CharField(max_length=20)), + ('method', models.CharField(max_length=20)), + ('date', models.DateField()), + ('bank_account', models.CharField(max_length=20, null=True)), + ('details', models.CharField(max_length=100, null=True)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.CharField(max_length=100, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, null=True)), + ('description', models.CharField(max_length=100, null=True)), + ('image_url', models.URLField(null=True)), + ], + ), + migrations.CreateModel( + name='ProductSale', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.FloatField()), + ('unit', models.CharField(max_length=10)), + ('unit_price', models.FloatField()), + ('addition', models.FloatField()), + ('price', models.FloatField()), + ('options_price', models.FloatField()), + ('total_price', models.FloatField()), + ('observations', models.CharField(max_length=100, null=True)), + ], + ), + migrations.CreateModel( + name='Sale', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total_sales', models.FloatField()), + ('total_discount', models.FloatField()), + ('total_shipping', models.FloatField()), + ('total_products', models.FloatField()), + ('order_amount', models.FloatField()), + ('prepaid_amount', models.FloatField()), + ('pending_amount', models.FloatField()), + ], + ), + migrations.CreateModel( + name='Transaction', + fields=[ + ('api_id', models.CharField(max_length=100, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField()), + ('closed_at', models.DateTimeField()), + ], + ), + ] diff --git a/src/site/api_transaction/migrations/0002_initial.py b/src/site/api_transaction/migrations/0002_initial.py new file mode 100644 index 00000000..ed08c143 --- /dev/null +++ b/src/site/api_transaction/migrations/0002_initial.py @@ -0,0 +1,82 @@ +# Generated by Django 4.1.5 on 2023-01-10 01:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('integrations', '0001_initial'), + ('api_transaction', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='transaction', + name='api_source', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='integrations.integrationtransaction'), + ), + migrations.AddField( + model_name='transaction', + name='sale', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api_transaction.sale'), + ), + migrations.AddField( + model_name='sale', + name='benefits', + field=models.ManyToManyField(to='api_transaction.benefit'), + ), + migrations.AddField( + model_name='sale', + name='client', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='api_transaction.client'), + ), + migrations.AddField( + model_name='sale', + name='delivery', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='api_transaction.delivery'), + ), + migrations.AddField( + model_name='sale', + name='fees', + field=models.ManyToManyField(to='api_transaction.fee'), + ), + migrations.AddField( + model_name='sale', + name='payments', + field=models.ManyToManyField(to='api_transaction.payment'), + ), + migrations.AddField( + model_name='sale', + name='products', + field=models.ManyToManyField(through='api_transaction.ProductSale', to='api_transaction.product'), + ), + migrations.AddField( + model_name='productsale', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api_transaction.product'), + ), + migrations.AddField( + model_name='productsale', + name='sale', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api_transaction.sale'), + ), + migrations.AddField( + model_name='ifood', + name='ifood_client_details', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='api_transaction.ifoodclient'), + ), + migrations.AddField( + model_name='ifood', + name='transaction', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api_transaction.transaction'), + ), + migrations.AddField( + model_name='delivery', + name='address', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='api_transaction.address'), + ), + ] diff --git a/src/site/api_transaction/migrations/0003_alter_transaction_closed_at.py b/src/site/api_transaction/migrations/0003_alter_transaction_closed_at.py new file mode 100644 index 00000000..606e1a0f --- /dev/null +++ b/src/site/api_transaction/migrations/0003_alter_transaction_closed_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.5 on 2023-01-11 20:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api_transaction', '0002_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='transaction', + name='closed_at', + field=models.DateTimeField(null=True), + ), + ] diff --git a/src/site/api_transaction/migrations/0004_alter_client_document_alter_client_email_and_more.py b/src/site/api_transaction/migrations/0004_alter_client_document_alter_client_email_and_more.py new file mode 100644 index 00000000..0d3bf26a --- /dev/null +++ b/src/site/api_transaction/migrations/0004_alter_client_document_alter_client_email_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.1.5 on 2023-01-11 20:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api_transaction', '0003_alter_transaction_closed_at'), + ] + + operations = [ + migrations.AlterField( + model_name='client', + name='document', + field=models.CharField(max_length=14, null=True, unique=True), + ), + migrations.AlterField( + model_name='client', + name='email', + field=models.EmailField(max_length=254, null=True, unique=True), + ), + migrations.AlterField( + model_name='client', + name='group', + field=models.CharField(max_length=100, null=True), + ), + migrations.AlterField( + model_name='client', + name='phone_number', + field=models.CharField(max_length=20, null=True, unique=True), + ), + ] diff --git a/src/site/api_transaction/migrations/0005_rename_total_sales_sale_total_fees.py b/src/site/api_transaction/migrations/0005_rename_total_sales_sale_total_fees.py new file mode 100644 index 00000000..3fff591e --- /dev/null +++ b/src/site/api_transaction/migrations/0005_rename_total_sales_sale_total_fees.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.5 on 2023-01-11 21:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api_transaction', '0004_alter_client_document_alter_client_email_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='sale', + old_name='total_sales', + new_name='total_fees', + ), + ] diff --git a/src/site/api_transaction/migrations/__init__.py b/src/site/api_transaction/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/site/api_transaction/models.py b/src/site/api_transaction/models.py new file mode 100644 index 00000000..7789cf23 --- /dev/null +++ b/src/site/api_transaction/models.py @@ -0,0 +1,177 @@ +from django.db import models +from integrations.models import IntegrationTransaction + + +class Client(models.Model): + + name = models.CharField(max_length=100, null=False, blank=False) + email = models.EmailField(null=True, unique=True) + phone_number = models.CharField(max_length=20, null=True, unique=True) + document = models.CharField(max_length=14, null=True, unique=True) + group = models.CharField(max_length=100, null=True) + id = models.CharField(max_length=100, primary_key=True) + + + +class Product(models.Model): + + id = models.CharField(max_length=100, primary_key=True) + name = models.CharField(max_length=100, null=True) + description = models.CharField(max_length=100, null=True) + image_url = models.URLField(null=True) + + + +class ProductSale(models.Model): + + product = models.ForeignKey(Product, on_delete=models.PROTECT) + sale = models.ForeignKey('Sale', on_delete=models.PROTECT) + quantity = models.FloatField(null=False, blank=False) + unit = models.CharField(max_length=10, null=False, blank=False) + + unit_price = models.FloatField(null=False, blank=False) + addition = models.FloatField(null=False, blank=False) + + # Price = Quantity * (unit_price + addition) + price = models.FloatField(null=False, blank=False) + + options_price = models.FloatField(null=False, blank=False) + + # Total Price = price + options_price + total_price = models.FloatField(null=False, blank=False) + + observations = models.CharField(max_length=100, null=True) + + + +class Address(models.Model): + + street_name = models.CharField(max_length=100, null=True) + street_number = models.CharField(max_length=10, null=True) + neighborhood = models.CharField(max_length=100, null=True) + complement = models.CharField(max_length=100, null=True) + reference = models.CharField(max_length=100, null=True) + postal_code = models.CharField(max_length=10, null=True) + city = models.CharField(max_length=100, null=True) + country = models.CharField(max_length=50, null=True) + latitude = models.CharField(max_length=15, null=True) + longitude = models.CharField(max_length=15, null=True) + + + +class Delivery(models.Model): + + # Mode = DEFAULT / EXPRESS / ECONOMIC / TAKE OUT + mode = models.CharField(max_length=20, null=True) + + # Delivery_by = IFOOD / MERCHANT / CORREIOS + delivery_by = models.CharField(max_length=20, null=True) + + delivery_date_time = models.DateTimeField(null=True) + observations = models.CharField(max_length=100, null=True) + address = models.ForeignKey(Address, on_delete=models.PROTECT, null=True) + + + +class Payment(models.Model): + + value = models.FloatField(null=False, blank=False) + currency = models.CharField(max_length=3, null=False, blank=False) + + # Type = ONLINE or OFFLINE + type = models.CharField(max_length=20, null=False, blank=False) + + # Method = CASH / CREDIT / DEBIT / PIX + method = models.CharField(max_length=20, null=False, blank=False) + + date = models.DateField() + bank_account = models.CharField(max_length=20, null=True) + + details = models.CharField(max_length=100, null=True) + + + +class Benefit(models.Model): + + value = models.FloatField(null=False, blank=False) + + # Target = CART / DELIVERY_FEE / ITEM / PROGRESSIVE + target = models.CharField(max_length=20, null=False, blank=False) + + # SPONSORSHIP = IFOOD / MERCHANT / EXTERNAL + sponsorship_name = models.CharField(max_length=20, null=False, blank=False) + sponsorship_description = models.CharField(max_length=100, null=False, blank=False) + + + +class Fee(models.Model): + + # Type = SMALL_ORDER + type = models.CharField(max_length=100, null=False) + + full_description = models.CharField(max_length=100, null=False) + value = models.FloatField(null=False, blank=False) + liabilities = models.CharField(max_length=200, null=False) + + + +class Sale(models.Model): + + total_products = models.FloatField(null=False, blank=False) + total_discount = models.FloatField(null=False, blank=False) + total_shipping = models.FloatField(null=False, blank=False) + total_fees = models.FloatField(null=False, blank=False) + + client = models.ForeignKey(Client, on_delete=models.PROTECT, null=True) + delivery = models.ForeignKey(Delivery, on_delete=models.PROTECT, null=True) + products = models.ManyToManyField(Product, through='ProductSale') + payments = models.ManyToManyField(Payment) + benefits = models.ManyToManyField(Benefit) + fees = models.ManyToManyField(Fee) + + order_amount = models.FloatField(null=False, blank=False) + prepaid_amount = models.FloatField(null=False, blank=False) + pending_amount = models.FloatField(null=False, blank=False) + + + + + +# Represents a sales transaction +class Transaction(models.Model): + + api_source = models.ForeignKey(IntegrationTransaction, on_delete=models.PROTECT) + api_id = models.CharField(max_length=100, primary_key=True) + created_at = models.DateTimeField() + closed_at = models.DateTimeField(null=True) + + models.UniqueConstraint(fields=['api', 'id_api'], name='unique_transaction') + + sale = models.ForeignKey(Sale, on_delete=models.PROTECT) + + + + +################################## +# IFOOD # +################################## + + +class IfoodClient(models.Model): + + orders_count = models.IntegerField(null=True) + segmentation = models.CharField(max_length=20, null=True) + +class Ifood(models.Model): + + transaction = models.ForeignKey(Transaction, on_delete=models.CASCADE) + display_id = models.CharField(max_length=10) + order_type = models.CharField(max_length=20) + order_timing = models.CharField(max_length=20) + sales_channel = models.CharField(max_length=30) + extra_info = models.CharField(max_length=100) + + ifood_client_details = models.ForeignKey(IfoodClient, on_delete=models.PROTECT, null=True) + + + \ No newline at end of file diff --git a/src/site/api_transaction/tests.py b/src/site/api_transaction/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/src/site/api_transaction/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/site/api_transaction/views.py b/src/site/api_transaction/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/src/site/api_transaction/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/src/site/erp/settings.py b/src/site/erp/settings.py index c33ba763..95a8c61a 100644 --- a/src/site/erp/settings.py +++ b/src/site/erp/settings.py @@ -13,6 +13,10 @@ from pathlib import Path import os + +load_dotenv() + + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -40,6 +44,7 @@ 'django.contrib.staticfiles', 'usuarios', 'rolepermissions', + "api_transaction" ] MIDDLEWARE = [ @@ -76,13 +81,31 @@ # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases +# Check host name (to validate if running Docker) +host = os.system("ping -c 1 db") +if host == 0: + print("Running Docker") + host = "db" +else: + print(host) + host = "localhost" + +# Check host name (to validate if running Docker) +host = os.system("ping -c 1 db") +if host == 0: + print("Running Docker") + host = "db" +else: + print(host) + host = "localhost" + DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "db", "USER": "user", "PASSWORD": "password", - "HOST": "localhost", + "HOST": host, "PORT": "5432", } } diff --git a/src/site/integrations/admin.py b/src/site/integrations/admin.py new file mode 100644 index 00000000..b032b593 --- /dev/null +++ b/src/site/integrations/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from .models import Integration, IntegrationTransaction + + +class IntegrationTransactionInline(admin.StackedInline): + model = IntegrationTransaction + +@admin.register(Integration) +class IntegrationAdmin(admin.ModelAdmin): + inlines = [IntegrationTransactionInline] diff --git a/src/site/integrations/migrations/0002_initial.py b/src/site/integrations/migrations/0002_initial.py new file mode 100644 index 00000000..3458bb65 --- /dev/null +++ b/src/site/integrations/migrations/0002_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.5 on 2023-01-10 01:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('integrations', '0001_initial'), + ('user', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='integration', + name='client_document', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='user.client'), + ), + ] diff --git a/src/site/integrations/models.py b/src/site/integrations/models.py new file mode 100644 index 00000000..37708ff4 --- /dev/null +++ b/src/site/integrations/models.py @@ -0,0 +1,23 @@ +from django.db import models +from user.models import Client + + +class Integration(models.Model): + + client_document = models.ForeignKey(Client, on_delete=models.PROTECT) + integration_name = models.CharField(max_length=100) + status = models.CharField(max_length=30) + + models.UniqueConstraint(fields=['client_document', 'integration_name'], name='unique_integration') + + def __str__(self): + return f'{self.client_document} - {self.integration_name}' + + def __hash__(self): + return hash((self.client_document, self.integration_name)) + + +class IntegrationTransaction(models.Model): + + integration = models.ForeignKey(Integration, on_delete=models.PROTECT, related_name='integration_transaction') + identifier = models.CharField(max_length=100) \ No newline at end of file diff --git a/src/site/manage.py b/src/site/manage.py old mode 100755 new mode 100644 index 06219288..6b01cfb6 --- a/src/site/manage.py +++ b/src/site/manage.py @@ -2,6 +2,7 @@ """Django's command-line utility for administrative tasks.""" import os import sys +import time def main(): @@ -18,5 +19,10 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": + + if sys.argv[1] == "runserver": + print("Waiting for database to be ready...") + time.sleep(15) + main() diff --git a/src/site/requirements.txt b/src/site/requirements.txt index 55a3af5e..8db8ec39 100644 --- a/src/site/requirements.txt +++ b/src/site/requirements.txt @@ -4,3 +4,5 @@ Pillow psycopg2-binary coveralls python-dotenv +requests +psycopg2 \ No newline at end of file diff --git a/src/site/user/admin.py b/src/site/user/admin.py new file mode 100644 index 00000000..8a5ff466 --- /dev/null +++ b/src/site/user/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from .models import Client, UserClient + +@admin.register(Client) +class ClientAdmin(admin.ModelAdmin): + list_display = ('document', 'nome_fantasia', 'razao_social') + list_editable = ('nome_fantasia', 'razao_social') + search_fields = ('document', 'nome_fantasia', 'razao_social') + + +@admin.register(UserClient) +class UserClientAdmin(admin.ModelAdmin): + list_display = ('username', 'whatsapp_number', 'telegram_username') + list_editable = ('whatsapp_number', 'telegram_username') + search_fields = ('username', 'user_document', 'whatsapp_number', 'telegram_username') \ No newline at end of file diff --git a/src/site/user/models.py b/src/site/user/models.py new file mode 100644 index 00000000..e38adb87 --- /dev/null +++ b/src/site/user/models.py @@ -0,0 +1,29 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + +class Client(models.Model): + + document = models.CharField(max_length=14, primary_key=True) + nome_fantasia = models.CharField(max_length=100) + razao_social = models.CharField(max_length=100) + + def __str__(self): + name = f'{self.document} - {self.razao_social}' + return name + + +class UserClient(AbstractUser): + + user_document = models.ManyToManyField(Client) + + whatsapp_number = models.CharField(max_length=20, unique=True, blank=True) + whatsapp_chat_id = models.CharField(max_length=20, unique=True, blank=True) + + telegram_username = models.CharField(max_length=40, unique=True, blank=True) + telegram_chat_id = models.CharField(max_length=20, unique=True, blank=True) + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['password'] + + def __str__(self): + return str(self.user_document) \ No newline at end of file diff --git a/src/site/usuarios/tests.py b/src/site/usuarios/tests.py index 7ce503c2..4298ace5 100644 --- a/src/site/usuarios/tests.py +++ b/src/site/usuarios/tests.py @@ -1,3 +1,4 @@ from django.test import TestCase +from .models import Integration # Create your tests here.