pySwivells 1.0.2__py3-none-any.whl

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.
pySwivells/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.2"
@@ -0,0 +1,3 @@
1
+ from .tags import read_tags
2
+ from .cleaning import clean_sales
3
+ from .reconciliation import reconcile
@@ -0,0 +1,150 @@
1
+ import pandas as pd
2
+ import math
3
+ import numpy as np
4
+ import pickle
5
+ import os
6
+
7
+ from pySwivells import iomix
8
+ from pySwivells.accounting.tva import TVA_RATES
9
+
10
+
11
+ def nullify(row):
12
+ for col in [
13
+ "Ventes brutes",
14
+ "Réductions",
15
+ "Retours",
16
+ "Ventes nettes",
17
+ "Frais d’expédition",
18
+ "Taxes",
19
+ "Ventes totales",
20
+ ]:
21
+ row[col] = 0
22
+
23
+
24
+ RULES = [
25
+ {
26
+ "tag": "lesbienfaiteurs",
27
+ "comment": "Vente les bien faiteurs",
28
+ "action": nullify,
29
+ },
30
+ {"tag": "Etsy", "comment": "Commande Etsy", "action": lambda r: r},
31
+ ]
32
+
33
+
34
+ def __load_tags():
35
+ if not os.path.exists("tags.pkl"):
36
+ iomix.warning("Aucun fichier de balises trouvé.")
37
+ rep = iomix.yes_or_no("Voulez-vous continuer sans balises ?")
38
+ if rep is False:
39
+ iomix.log("Opération terminée par l'utilisateur.")
40
+ iomix.log(
41
+ "Veuillez d'abord exécuter 'pyswivells compta tags <FICHIER.csv>'"
42
+ )
43
+ exit(1)
44
+ else:
45
+ return {}
46
+
47
+ with open("tags.pkl", "rb") as f:
48
+ return pickle.load(f)
49
+
50
+
51
+ def __apply_rules(df):
52
+ tag_map = __load_tags()
53
+ modified_rows = []
54
+ for _, row in df.iterrows():
55
+ order = row["Nom de la commande"]
56
+ tags = tag_map.get(order)
57
+ row["Comment"] = ""
58
+ if tags:
59
+ for rule in RULES:
60
+ if rule["tag"] in tags:
61
+ row["Comment"] += rule["comment"] + "; "
62
+ rule["action"](row)
63
+ modified_rows.append(row)
64
+
65
+ df = pd.DataFrame(modified_rows)
66
+ return df
67
+
68
+
69
+ def __calculate_tva(df) -> float:
70
+ df["TVA"] = df["Pays d’expédition"].map(TVA_RATES)
71
+ df["Taxes calculées"] = round(
72
+ round(df["Ventes totales"] / (1 + df["TVA"] / 100), 2) * df["TVA"] / 100,
73
+ 2,
74
+ )
75
+ df["Différence"] = df["Taxes"] - df["Taxes calculées"]
76
+ df["Erreur de TVA"] = df["Différence"].apply(
77
+ lambda x: "ERREUR" if abs(x) > 0.01 else ""
78
+ )
79
+
80
+
81
+ def __clean_lines(df):
82
+ rows_to_keep = []
83
+ orders = df["Nom de la commande"].unique()
84
+ for order in orders:
85
+ order_df_all_days = df[df["Nom de la commande"] == order]
86
+ for day in order_df_all_days["Jour"].unique():
87
+ order_df = order_df_all_days[order_df_all_days["Jour"] == day]
88
+ values = list(
89
+ set([math.fabs(v) for v in order_df["Ventes totales"].unique()])
90
+ )
91
+ d = {v: {"+": 0, "-": 0} for v in values}
92
+ for _, row in order_df.iterrows():
93
+ sign = "+" if row["Ventes totales"] > 0 else "-"
94
+ d[math.fabs(row["Ventes totales"])][sign] += 1
95
+ for k, v in d.items():
96
+ if v["+"] > v["-"]:
97
+ value_to_keep = float(k)
98
+ elif v["+"] < v["-"]:
99
+ value_to_keep = -float(k)
100
+ else:
101
+ continue
102
+ sub_df = order_df[
103
+ order_df["Ventes totales"] == value_to_keep
104
+ ].reset_index(drop=True)
105
+ if len(sub_df) > 1:
106
+ sub_df = order_df[
107
+ (order_df["Ventes totales"] == value_to_keep)
108
+ & (order_df["Titre de produit au moment de la vente"])
109
+ ].reset_index(drop=True)
110
+ if len(sub_df) > 1:
111
+ counter = 0
112
+ for _, r in sub_df.iterrows():
113
+ counter += 1
114
+ rows_to_keep.append(r)
115
+ if counter == abs(v["+"] - v["-"]):
116
+ break
117
+ elif len(sub_df) == 1:
118
+ rows_to_keep.append(sub_df.iloc[0])
119
+ else:
120
+ sub_df = order_df[
121
+ (order_df["Ventes totales"] == value_to_keep)
122
+ ].reset_index(drop=True)
123
+ rows_to_keep.append(sub_df.iloc[0])
124
+
125
+ else:
126
+ rows_to_keep.append(sub_df.iloc[0])
127
+ return pd.DataFrame(rows_to_keep)
128
+
129
+
130
+ def clean_file(sales_file: str) -> pd.DataFrame:
131
+ df = pd.read_csv(sales_file)
132
+ df = df.replace({np.nan: None})
133
+ df = __apply_rules(df)
134
+ __calculate_tva(df)
135
+
136
+ cleaned_df = __clean_lines(df)
137
+ cleaned_df = cleaned_df[
138
+ cleaned_df["Titre de produit au moment de la vente"] != "Tote bag noir Swivells"
139
+ ].reset_index(drop=True)
140
+ return cleaned_df
141
+
142
+
143
+ def clean_sales(sales_file: str):
144
+ final_df = clean_file(sales_file)
145
+ final_df.to_excel("Ventes-nettoyées.xlsx", index=False)
146
+
147
+
148
+ def clean_sales_to_csv(sales_file: str, output_file: str):
149
+ final_df = clean_file(sales_file)
150
+ final_df.to_csv(output_file, index=False)
@@ -0,0 +1,136 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import os
4
+
5
+ from pySwivells.iomix import error
6
+
7
+
8
+ def __get_files(directory: str):
9
+ files = []
10
+ for filename in os.listdir(directory):
11
+ if filename.endswith(".csv") or filename.endswith(".xlsx"):
12
+ files.append(os.path.join(directory, filename))
13
+ return files
14
+
15
+
16
+ def __get_sales():
17
+ if not os.path.exists("Ventes-nettoyées.xlsx"):
18
+ error(
19
+ "Le fichier 'Ventes-nettoyées.xlsx' est introuvable. Veuillez exécuter d'abord le nettoyage des ventes."
20
+ )
21
+ df = pd.read_excel("Ventes-nettoyées.xlsx")
22
+ df = df.replace({np.nan: None})
23
+ return df
24
+
25
+
26
+ def __process_payment_file(file_path: str, payments: dict):
27
+ if file_path.endswith(".csv"):
28
+ df = pd.read_csv(file_path)
29
+ elif file_path.endswith(".xlsx"):
30
+ df = pd.read_excel(file_path)
31
+ else:
32
+ error(f"Format de fichier non pris en charge : {file_path}")
33
+ df = df.replace({np.nan: None})
34
+ payouts = df["Payout Date"].unique()
35
+ for payout in payouts:
36
+ payout_df = df[df["Payout Date"] == payout]
37
+ for _, row in payout_df.iterrows():
38
+ order = row["Order"]
39
+ amount = row["Amount"]
40
+ fee = row["Fee"]
41
+ net = row["Net"]
42
+ if order not in payments:
43
+ payments[order] = []
44
+ payments[order].append(
45
+ {
46
+ "payout_date": payout,
47
+ "amount": amount,
48
+ "fee": fee,
49
+ "net": net,
50
+ "source_file": os.path.basename(file_path),
51
+ }
52
+ )
53
+
54
+
55
+ def reconcile(payments_dir: str):
56
+ df = __get_sales()
57
+ df["Date du paiement"] = ""
58
+ df["Relevé"] = ""
59
+ df["CA"] = ""
60
+ df["SHP Payment fee"] = ""
61
+ df["Net reçu"] = ""
62
+ payment_files = __get_files(payments_dir)
63
+ payments = {}
64
+ final_df = []
65
+ for p in payment_files:
66
+ __process_payment_file(p, payments)
67
+ df = pd.read_excel("Ventes-nettoyées.xlsx")
68
+ order_without_payment = set()
69
+ order_to_check = set()
70
+ for _, row in df.iterrows():
71
+ row["Réconciliation"] = "✅"
72
+ order = row["Nom de la commande"]
73
+ payouts = payments.get(order, [])
74
+ if len(payouts) == 0:
75
+ row["Réconciliation"] = "❌"
76
+ row["Commentaire"] = "Pas de paiement trouvé"
77
+ order_without_payment.add(order)
78
+ elif len(payouts) == 1:
79
+ order_df = df[df["Nom de la commande"] == order]
80
+ total_amount = order_df["Ventes totales"].sum()
81
+ row["Date du paiement"] = payouts[0]["payout_date"]
82
+ row["Relevé"] = payouts[0]["source_file"]
83
+ row["CA"] = payouts[0]["amount"]
84
+ row["SHP Payment fee"] = payouts[0]["fee"]
85
+ row["Net reçu"] = payouts[0]["net"]
86
+ if abs(payouts[0]["amount"] - total_amount) > 0.01:
87
+ row["Réconciliation"] = "❌"
88
+ row["Commentaire"] = "Montant du paiement incorrect"
89
+ order_to_check.add(order)
90
+ else:
91
+ order_df = df[df["Nom de la commande"] == order]
92
+ for d in order_df["Jour"].unique():
93
+ sub_order_df = order_df[order_df["Jour"] == d]
94
+ total_amount = sub_order_df["Ventes totales"].sum()
95
+ matched = __find_matching_payment(total_amount, payouts, row)
96
+ if not matched:
97
+ payout_df = pd.DataFrame(payouts)
98
+ grouped = (
99
+ payout_df.groupby("payout_date")
100
+ .agg(
101
+ {
102
+ "amount": "sum",
103
+ "fee": "sum",
104
+ "net": "sum",
105
+ "source_file": lambda x: ",".join(
106
+ x.unique()
107
+ ), # optional: combine unique source files
108
+ }
109
+ )
110
+ .reset_index()
111
+ )
112
+ payouts = grouped.to_dict(orient="records")
113
+ matched = __find_matching_payment(total_amount, payouts, row)
114
+ if not matched:
115
+ print("no match after grouping...")
116
+ row["Réconciliation"] = "❌"
117
+ row["Commentaire"] = "Montant du paiement incorrect"
118
+ order_to_check.add(order)
119
+ final_df.append(row)
120
+ print(f"{len(order_without_payment)} commandes sans paiement trouvé")
121
+ print(f"{len(order_to_check)} commandes avec des montants à vérifier")
122
+ print("Fichier Excel sauvegardé sous 'Reconciliation.xlsx'")
123
+ final_df = pd.DataFrame(final_df)
124
+ final_df.to_excel("Reconciliation.xlsx", index=False)
125
+
126
+
127
+ def __find_matching_payment(total_amount: float, payouts: list, row) -> bool:
128
+ for p in payouts:
129
+ if abs(p["amount"] - total_amount) < 0.01:
130
+ row["Date du paiement"] = p["payout_date"]
131
+ row["Relevé"] = p["source_file"]
132
+ row["CA"] = p["amount"]
133
+ row["SHP Payment fee"] = p["fee"]
134
+ row["Net reçu"] = p["net"]
135
+ return True
136
+ return False
@@ -0,0 +1,19 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import pickle
4
+
5
+
6
+ def read_tags(tag_file: str):
7
+ tags = pd.read_csv(tag_file)
8
+ tags = tags.replace({np.nan: None})
9
+ TAGS = {}
10
+ for _, row in tags.iterrows():
11
+ order = row["Nom de la commande"]
12
+ if row["Balises de commande"]:
13
+ if TAGS.get(order):
14
+ TAGS[order].append(row["Balises de commande"])
15
+ else:
16
+ TAGS[order] = [row["Balises de commande"]]
17
+
18
+ with open("tags.pkl", "wb") as f:
19
+ pickle.dump(TAGS, f)
@@ -0,0 +1,49 @@
1
+ TVA_RATES = {
2
+ "Malta": 18.00,
3
+ "Netherlands": 21.00,
4
+ "Greece": 24.00,
5
+ "Bulgaria": 20.00,
6
+ "Australia": 0.00,
7
+ "Italy": 22.00,
8
+ "New Zealand": 0.00,
9
+ "Lithuania": 21.00,
10
+ "Hong Kong": 0.00,
11
+ "Austria": 20.00,
12
+ "Hong Kong": 0.00,
13
+ "Czech Republic": 21.00,
14
+ "Belgium": 21.00,
15
+ "Switzerland": 0.00,
16
+ "Portugal": 22.00,
17
+ "Slovakia": 20.00,
18
+ "Slovenia": 22.00,
19
+ "United States": 0.00,
20
+ "Canada": 0.00,
21
+ "Thailand": 0.00,
22
+ "Spain": 21.00,
23
+ "France": 20.00,
24
+ "Ireland": 23.00,
25
+ "Luxembourg": 17.00,
26
+ "Cyprus": 19.00,
27
+ "Poland": 23.00,
28
+ "Croatia": 25.00,
29
+ "Norway": 0.00,
30
+ "Denmark": 25.00,
31
+ "Hungary": 27.00,
32
+ "Romania": 19.00,
33
+ "Estonia": 24.00,
34
+ "Sweden": 25.00,
35
+ "Serbia": 0.00,
36
+ "Finland": 25.50,
37
+ "Japan": 0.00,
38
+ "United Kingdom": 0.00,
39
+ "Latvia": 21.00,
40
+ "Reunion": 0.00,
41
+ "Germany": 19.00,
42
+ "Israel": 0.00,
43
+ "Taiwan": 0.00,
44
+ "South Korea": 0.00,
45
+ "New Caledonia": 0.00,
46
+ "Singapore": 0.00,
47
+ "Iceland": 0.00,
48
+ "Belarus": 0.00,
49
+ }
pySwivells/cli.py ADDED
@@ -0,0 +1,58 @@
1
+ import fire
2
+
3
+ from pySwivells import accounting
4
+
5
+ from pySwivells.iomix import log, Colors
6
+
7
+
8
+ class PySwivellsCLI:
9
+ """PySwivells command-line interface."""
10
+
11
+ class compta:
12
+ """Fonctions de comptabilité.
13
+
14
+ Étapes :
15
+ 1. Récupérer le tableau des balises des commandes
16
+ 2. Lancer le script de lecture des balises : pyswivells compta tags <TABLEAU-DES-BALISES.csv>
17
+ 3. Récupérer du tableau des ventes Shopify.
18
+ 4. Lancer le nettoyage : pyswivells compta clean <TABLEAU-DES-VENTES.csv>
19
+ 5. Vérifier le tableau nettoyé pour d'éventuelles erreurs de TVA.
20
+ 6. Exporter les relevés d'encaissement Shopify.
21
+ 7. Placer tous les relevés d'encaissement Shopify dans un seul dossier <DOSSIER>.
22
+ 8. Vérifier la correspondance entre les ventes et les encaissements : pyswivells compta reconcile <DOSSIER>
23
+ """
24
+
25
+ def tags(self, TABLEAU_DES_BALISES_CSV: str):
26
+ """Lit les balises des commandes depuis un fichier CSV.
27
+
28
+ Args:
29
+ TABLEAU_DES_BALISES_CSV: Chemin vers le fichier CSV des balises des commandes.
30
+ """
31
+ accounting.read_tags(TABLEAU_DES_BALISES_CSV)
32
+ log(
33
+ f"Balises lues et enregistrées depuis {TABLEAU_DES_BALISES_CSV} ✅",
34
+ )
35
+
36
+ def clean(self, TABLEAU_DES_VENTES_EXCEL: str):
37
+ """Nettoie le tableau des ventes Shopify.
38
+
39
+ Args:
40
+ TABLEAU_DES_VENTES: Chemin vers le fichier Excel des ventes Shopify.
41
+ """
42
+ accounting.clean_sales(TABLEAU_DES_VENTES_EXCEL)
43
+
44
+ def reconcile(self, DOSSIER: str):
45
+ """Vérifie la correspondance entre les ventes et les encaissements.
46
+
47
+ Args:
48
+
49
+ DOSSIER: Chemin vers le dossier contenant les relevés d'encaissement.
50
+ """
51
+ accounting.reconcile(
52
+ DOSSIER,
53
+ )
54
+
55
+
56
+ def main():
57
+ # Fire automatically exposes all methods as CLI commands
58
+ fire.Fire(PySwivellsCLI)
pySwivells/iomix.py ADDED
@@ -0,0 +1,35 @@
1
+ class Colors:
2
+ HEADER = "\033[95m"
3
+ OKBLUE = "\033[94m"
4
+ OKCYAN = "\033[96m"
5
+ OKGREEN = "\033[92m"
6
+ WARNING = "\033[93m"
7
+ FAIL = "\033[91m"
8
+ ENDC = "\033[0m"
9
+ BOLD = "\033[1m"
10
+ UNDERLINE = "\033[4m"
11
+
12
+
13
+ def log(message: str, color: str = Colors.ENDC):
14
+ print(f"{color}{message}{Colors.ENDC}")
15
+
16
+
17
+ def error(message: str, exit_app: bool = True):
18
+ log(f"=== ERREUR ===\n{message}", Colors.FAIL)
19
+ if exit_app:
20
+ exit(-1)
21
+
22
+
23
+ def warning(message: str):
24
+ log(f"=== AVERTISSEMENT ===\n{message}", Colors.WARNING)
25
+
26
+
27
+ def yes_or_no(prompt: str) -> bool:
28
+ while True:
29
+ response = input(f"{prompt} (y/n): ").strip().lower()
30
+ if response in ["y", "yes"]:
31
+ return True
32
+ elif response in ["n", "no"]:
33
+ return False
34
+ else:
35
+ print("Veuillez répondre par 'y' ou 'n'.")
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: pySwivells
3
+ Version: 1.0.2
4
+ Summary: python utilities for Swivells
5
+ Author-email: Arnaud Hoffmann <info@css23.fr>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pandas
14
+ Requires-Dist: openpyxl
15
+ Requires-Dist: requests
16
+ Requires-Dist: rich
17
+ Requires-Dist: fire
@@ -0,0 +1,13 @@
1
+ pySwivells/__init__.py,sha256=Y3LSfRioSl2xch70pq_ULlvyECXyEtN3krVaWeGyaxk,22
2
+ pySwivells/cli.py,sha256=Ca4HhDSjGPsLq7Kb4rB_qFSQCz_oPdQ_cN6Ksbk4xTU,2032
3
+ pySwivells/iomix.py,sha256=dBO_9ZT1N3bROvfxi9eOUUsqDtXF0ha7N-zl2BRzKjM,860
4
+ pySwivells/accounting/__init__.py,sha256=L0DD2QiBy5pdsYn1U91rkqEpEczkQgDxfMY8psLOM8k,100
5
+ pySwivells/accounting/cleaning.py,sha256=CUZAXQd2a2YtbRVNAOqN55QuEmkSeRW1EjpzwmcXwQc,4783
6
+ pySwivells/accounting/reconciliation.py,sha256=qaoyP8cs0FdOdQDC7SNMeTbWYrjN7BqxZNDqUHGypls,5251
7
+ pySwivells/accounting/tags.py,sha256=RnbXni6Z92rVAYCn0RUGRyJF01pUi2ebE182wqyskUg,528
8
+ pySwivells/accounting/tva.py,sha256=5L93bneRDRWobnGhvcl1Wr1B0eMkFV89DcQb9GtWkrs,1071
9
+ pyswivells-1.0.2.dist-info/METADATA,sha256=TVUHytQCvnDYXj1o-Fx0ix_ed0B8ZyzCl33jGAcx0KI,492
10
+ pyswivells-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ pyswivells-1.0.2.dist-info/entry_points.txt,sha256=GTS7Ok58WR4iqF9B7aHU0KjgQTC7BOS4VaIZ50YGrNo,51
12
+ pyswivells-1.0.2.dist-info/top_level.txt,sha256=NFYTwTiUdwLbCA-3LZrEc7YBg8W-MnxhkSofr_BQoJQ,11
13
+ pyswivells-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyswivells = pySwivells.cli:main
@@ -0,0 +1 @@
1
+ pySwivells