Spaces:
Sleeping
Sleeping
EL GHAFRAOUI AYOUB
commited on
Commit
·
d749575
1
Parent(s):
7ede824
'C'
Browse files- alembic/versions/9db9ce94286f_add_commercial_field.py +65 -0
- alembic/versions/__pycache__/9db9ce94286f_add_commercial_field.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/db/__pycache__/models.cpython-312.pyc +0 -0
- app/db/models.py +1 -0
- app/main.py +1 -0
- app/routes/__pycache__/__init__.cpython-312.pyc +0 -0
- app/routes/__pycache__/invoices.cpython-312.pyc +0 -0
- app/routes/invoices.py +2 -1
- app/schemas/__pycache__/invoice.cpython-312.pyc +0 -0
- app/schemas/invoice.py +1 -0
- app/services/__pycache__/invoice_service.cpython-312.pyc +0 -0
- app/services/invoice_service.py +46 -13
- app/templates/index.html +14 -2
- sql_app.db +0 -0
alembic/versions/9db9ce94286f_add_commercial_field.py
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""add_commercial_field
|
2 |
+
|
3 |
+
Revision ID: 9db9ce94286f
|
4 |
+
Revises:
|
5 |
+
Create Date: 2025-01-29 13:45:40.493950
|
6 |
+
|
7 |
+
"""
|
8 |
+
from typing import Sequence, Union
|
9 |
+
|
10 |
+
from alembic import op
|
11 |
+
import sqlalchemy as sa
|
12 |
+
|
13 |
+
|
14 |
+
# revision identifiers, used by Alembic.
|
15 |
+
revision: str = '9db9ce94286f'
|
16 |
+
down_revision: Union[str, None] = None
|
17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
19 |
+
|
20 |
+
|
21 |
+
def upgrade() -> None:
|
22 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
23 |
+
op.create_table('invoices',
|
24 |
+
sa.Column('id', sa.Integer(), nullable=False),
|
25 |
+
sa.Column('invoice_number', sa.String(), nullable=True),
|
26 |
+
sa.Column('date', sa.DateTime(), nullable=True),
|
27 |
+
sa.Column('project', sa.String(), nullable=True),
|
28 |
+
sa.Column('client_name', sa.String(), nullable=True),
|
29 |
+
sa.Column('client_phone', sa.String(), nullable=True),
|
30 |
+
sa.Column('address', sa.String(), nullable=True),
|
31 |
+
sa.Column('total_ht', sa.Float(), nullable=True),
|
32 |
+
sa.Column('tax', sa.Float(), nullable=True),
|
33 |
+
sa.Column('total_ttc', sa.Float(), nullable=True),
|
34 |
+
sa.Column('frame_number', sa.String(), nullable=True),
|
35 |
+
sa.Column('customer_name', sa.String(), nullable=True),
|
36 |
+
sa.Column('amount', sa.Float(), nullable=True),
|
37 |
+
sa.Column('status', sa.String(), nullable=True),
|
38 |
+
sa.Column('created_at', sa.DateTime(), nullable=True),
|
39 |
+
sa.Column('commercial', sa.String(), nullable=True),
|
40 |
+
sa.PrimaryKeyConstraint('id')
|
41 |
+
)
|
42 |
+
op.create_index(op.f('ix_invoices_id'), 'invoices', ['id'], unique=False)
|
43 |
+
op.create_table('invoice_items',
|
44 |
+
sa.Column('id', sa.Integer(), nullable=False),
|
45 |
+
sa.Column('invoice_id', sa.Integer(), nullable=True),
|
46 |
+
sa.Column('description', sa.String(), nullable=True),
|
47 |
+
sa.Column('unit', sa.String(), nullable=True),
|
48 |
+
sa.Column('quantity', sa.Integer(), nullable=True),
|
49 |
+
sa.Column('length', sa.Float(), nullable=True),
|
50 |
+
sa.Column('unit_price', sa.Float(), nullable=True),
|
51 |
+
sa.Column('total_price', sa.Float(), nullable=True),
|
52 |
+
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ),
|
53 |
+
sa.PrimaryKeyConstraint('id')
|
54 |
+
)
|
55 |
+
op.create_index(op.f('ix_invoice_items_id'), 'invoice_items', ['id'], unique=False)
|
56 |
+
# ### end Alembic commands ###
|
57 |
+
|
58 |
+
|
59 |
+
def downgrade() -> None:
|
60 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
61 |
+
op.drop_index(op.f('ix_invoice_items_id'), table_name='invoice_items')
|
62 |
+
op.drop_table('invoice_items')
|
63 |
+
op.drop_index(op.f('ix_invoices_id'), table_name='invoices')
|
64 |
+
op.drop_table('invoices')
|
65 |
+
# ### end Alembic commands ###
|
alembic/versions/__pycache__/9db9ce94286f_add_commercial_field.cpython-312.pyc
ADDED
Binary file (4.86 kB). View file
|
|
app/__pycache__/main.cpython-312.pyc
CHANGED
Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ
|
|
app/db/__pycache__/models.cpython-312.pyc
CHANGED
Binary files a/app/db/__pycache__/models.cpython-312.pyc and b/app/db/__pycache__/models.cpython-312.pyc differ
|
|
app/db/models.py
CHANGED
@@ -24,6 +24,7 @@ class Invoice(Base):
|
|
24 |
amount = Column(Float)
|
25 |
status = Column(String, default="pending")
|
26 |
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
27 |
|
28 |
items = relationship("InvoiceItem", back_populates="invoice")
|
29 |
|
|
|
24 |
amount = Column(Float)
|
25 |
status = Column(String, default="pending")
|
26 |
created_at = Column(DateTime, default=datetime.utcnow)
|
27 |
+
commercial = Column(String)
|
28 |
|
29 |
items = relationship("InvoiceItem", back_populates="invoice")
|
30 |
|
app/main.py
CHANGED
@@ -13,6 +13,7 @@ load_dotenv()
|
|
13 |
# Get the absolute path to the app directory
|
14 |
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
15 |
|
|
|
16 |
app = FastAPI(
|
17 |
title="Invoice Generator",
|
18 |
description="API for generating invoices",
|
|
|
13 |
# Get the absolute path to the app directory
|
14 |
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
15 |
|
16 |
+
|
17 |
app = FastAPI(
|
18 |
title="Invoice Generator",
|
19 |
description="API for generating invoices",
|
app/routes/__pycache__/__init__.cpython-312.pyc
CHANGED
Binary files a/app/routes/__pycache__/__init__.cpython-312.pyc and b/app/routes/__pycache__/__init__.cpython-312.pyc differ
|
|
app/routes/__pycache__/invoices.cpython-312.pyc
CHANGED
Binary files a/app/routes/__pycache__/invoices.cpython-312.pyc and b/app/routes/__pycache__/invoices.cpython-312.pyc differ
|
|
app/routes/invoices.py
CHANGED
@@ -47,7 +47,8 @@ async def create_new_invoice(
|
|
47 |
total_ht=invoice.total_ht,
|
48 |
tax=invoice.tax,
|
49 |
total_ttc=invoice.total_ttc,
|
50 |
-
frame_number=invoice.frame_number
|
|
|
51 |
)
|
52 |
|
53 |
db.add(db_invoice)
|
|
|
47 |
total_ht=invoice.total_ht,
|
48 |
tax=invoice.tax,
|
49 |
total_ttc=invoice.total_ttc,
|
50 |
+
frame_number=invoice.frame_number,
|
51 |
+
commercial=invoice.commercial
|
52 |
)
|
53 |
|
54 |
db.add(db_invoice)
|
app/schemas/__pycache__/invoice.cpython-312.pyc
CHANGED
Binary files a/app/schemas/__pycache__/invoice.cpython-312.pyc and b/app/schemas/__pycache__/invoice.cpython-312.pyc differ
|
|
app/schemas/invoice.py
CHANGED
@@ -25,6 +25,7 @@ class InvoiceCreate(BaseModel):
|
|
25 |
total_ttc: float
|
26 |
frame_number: Optional[str] = None
|
27 |
items: List[InvoiceItemCreate]
|
|
|
28 |
|
29 |
@computed_field
|
30 |
def customer_name(self) -> str:
|
|
|
25 |
total_ttc: float
|
26 |
frame_number: Optional[str] = None
|
27 |
items: List[InvoiceItemCreate]
|
28 |
+
commercial: Optional[str] = "divers"
|
29 |
|
30 |
@computed_field
|
31 |
def customer_name(self) -> str:
|
app/services/__pycache__/invoice_service.cpython-312.pyc
CHANGED
Binary files a/app/services/__pycache__/invoice_service.cpython-312.pyc and b/app/services/__pycache__/invoice_service.cpython-312.pyc differ
|
|
app/services/invoice_service.py
CHANGED
@@ -153,6 +153,10 @@ class InvoiceService:
|
|
153 |
|
154 |
current_y -= LINE_HEIGHT
|
155 |
|
|
|
|
|
|
|
|
|
156 |
def draw_item_row(item, indent=False):
|
157 |
nonlocal current_y
|
158 |
pdf.setFillColorRGB(*BLACK)
|
@@ -165,8 +169,8 @@ class InvoiceService:
|
|
165 |
(item.unit, 50),
|
166 |
(str(item.quantity), 50),
|
167 |
(f"{item.length:.2f}", 60),
|
168 |
-
(f"{item.unit_price
|
169 |
-
(f"{item.total_price
|
170 |
]
|
171 |
|
172 |
for value, width in cells:
|
@@ -192,13 +196,36 @@ class InvoiceService:
|
|
192 |
for item in items:
|
193 |
draw_item_row(item, indent=(keyword != "lfflflflf"))
|
194 |
|
195 |
-
# NB box with
|
196 |
nb_box_width = 200
|
197 |
nb_box_height = 80
|
198 |
pdf.setFillColorRGB(*BLACK)
|
199 |
pdf.rect(20, current_y - nb_box_height, nb_box_width, nb_box_height, stroke=1)
|
200 |
pdf.setFont("Helvetica-Bold", 12) # Made slightly larger for better visibility
|
201 |
-
pdf.drawString(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
|
203 |
# Totals section
|
204 |
current_y -= 20
|
@@ -206,12 +233,12 @@ class InvoiceService:
|
|
206 |
row_height = 20
|
207 |
|
208 |
for i, (label1, label2, value) in enumerate([
|
209 |
-
("Total", "H.T", f"{data.total_ht
|
210 |
-
("TVA", "20 %", f"{data.tax
|
211 |
-
("Total", "TTC", f"{data.total_ttc
|
212 |
]):
|
213 |
y = current_y - (i * row_height)
|
214 |
-
totals_x = (page_width - totals_table_width) - 27
|
215 |
draw_box(pdf, totals_x, y, totals_table_width / 2, row_height)
|
216 |
draw_box(pdf, totals_x + totals_table_width / 2, y, totals_table_width / 2, row_height)
|
217 |
pdf.drawString(totals_x + 10, y + 6, f"{label1} {label2}")
|
@@ -219,13 +246,19 @@ class InvoiceService:
|
|
219 |
|
220 |
# Footer
|
221 |
pdf.setFont("Helvetica", 8)
|
222 |
-
if os.path.exists(logo_path):
|
223 |
-
pdf.drawImage(logo_path, MARGIN, 20, width=30, height=15)
|
224 |
-
|
225 |
footer_text = "Douar Ait Laarassi Tidili, Cercle El Kelâa, Route de Safi, Km 14-40000 Marrakech"
|
226 |
pdf.drawCentredString(page_width / 2, 30, footer_text)
|
227 |
-
|
228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
|
230 |
pdf.save()
|
231 |
buffer.seek(0)
|
|
|
153 |
|
154 |
current_y -= LINE_HEIGHT
|
155 |
|
156 |
+
def format_currency(value):
|
157 |
+
# Format with 2 decimal places and thousands separator
|
158 |
+
return "{:,.2f}".format(value).replace(",", " ")
|
159 |
+
|
160 |
def draw_item_row(item, indent=False):
|
161 |
nonlocal current_y
|
162 |
pdf.setFillColorRGB(*BLACK)
|
|
|
169 |
(item.unit, 50),
|
170 |
(str(item.quantity), 50),
|
171 |
(f"{item.length:.2f}", 60),
|
172 |
+
(f"{format_currency(item.unit_price)} DH", 60),
|
173 |
+
(f"{format_currency(item.total_price)} DH", 170)
|
174 |
]
|
175 |
|
176 |
for value, width in cells:
|
|
|
196 |
for item in items:
|
197 |
draw_item_row(item, indent=(keyword != "lfflflflf"))
|
198 |
|
199 |
+
# NB box with text
|
200 |
nb_box_width = 200
|
201 |
nb_box_height = 80
|
202 |
pdf.setFillColorRGB(*BLACK)
|
203 |
pdf.rect(20, current_y - nb_box_height, nb_box_width, nb_box_height, stroke=1)
|
204 |
pdf.setFont("Helvetica-Bold", 12) # Made slightly larger for better visibility
|
205 |
+
pdf.drawString(30, current_y - nb_box_height + 60, "NB:")
|
206 |
+
|
207 |
+
# Add the new text
|
208 |
+
pdf.setFont("Helvetica", 8) # Smaller font for the note
|
209 |
+
nb_text = "Toute modification apportée aux plans BA initialement fournis, entraine automatiquement la modification de ce devis."
|
210 |
+
# Split text to fit in box
|
211 |
+
words = nb_text.split()
|
212 |
+
lines = []
|
213 |
+
current_line = []
|
214 |
+
|
215 |
+
for word in words:
|
216 |
+
current_line.append(word)
|
217 |
+
# Check if current line width exceeds box width
|
218 |
+
if pdf.stringWidth(' '.join(current_line), "Helvetica", 8) > nb_box_width - 20:
|
219 |
+
current_line.pop() # Remove last word
|
220 |
+
lines.append(' '.join(current_line))
|
221 |
+
current_line = [word]
|
222 |
+
|
223 |
+
if current_line:
|
224 |
+
lines.append(' '.join(current_line))
|
225 |
+
|
226 |
+
# Draw each line
|
227 |
+
for i, line in enumerate(lines):
|
228 |
+
pdf.drawString(30, current_y - nb_box_height + 45 - (i * 10), line)
|
229 |
|
230 |
# Totals section
|
231 |
current_y -= 20
|
|
|
233 |
row_height = 20
|
234 |
|
235 |
for i, (label1, label2, value) in enumerate([
|
236 |
+
("Total", "H.T", f"{format_currency(data.total_ht)} DH"),
|
237 |
+
("TVA", "20 %", f"{format_currency(data.tax)} DH"),
|
238 |
+
("Total", "TTC", f"{format_currency(data.total_ttc)} DH")
|
239 |
]):
|
240 |
y = current_y - (i * row_height)
|
241 |
+
totals_x = (page_width - totals_table_width) - 27
|
242 |
draw_box(pdf, totals_x, y, totals_table_width / 2, row_height)
|
243 |
draw_box(pdf, totals_x + totals_table_width / 2, y, totals_table_width / 2, row_height)
|
244 |
pdf.drawString(totals_x + 10, y + 6, f"{label1} {label2}")
|
|
|
246 |
|
247 |
# Footer
|
248 |
pdf.setFont("Helvetica", 8)
|
|
|
|
|
|
|
249 |
footer_text = "Douar Ait Laarassi Tidili, Cercle El Kelâa, Route de Safi, Km 14-40000 Marrakech"
|
250 |
pdf.drawCentredString(page_width / 2, 30, footer_text)
|
251 |
+
|
252 |
+
# Add commercial info to footer
|
253 |
+
print(f"Commercial value: {data.commercial}") # Add this debug line
|
254 |
+
if data.commercial and data.commercial.lower() != 'divers':
|
255 |
+
commercial_text = f"Commercial: {data.commercial.upper()}"
|
256 |
+
pdf.drawString(MARGIN, 20, commercial_text)
|
257 |
+
footer_contact = "Tél: 05 24 01 55 54 Fax : 05 24 01 55 29 E-mail : [email protected]"
|
258 |
+
pdf.drawCentredString(page_width / 2, 20, footer_contact)
|
259 |
+
else:
|
260 |
+
footer_contact = "Tél: 05 24 01 55 54 Fax : 05 24 01 55 29 E-mail : [email protected]"
|
261 |
+
pdf.drawCentredString(page_width / 2, 20, footer_contact)
|
262 |
|
263 |
pdf.save()
|
264 |
buffer.seek(0)
|
app/templates/index.html
CHANGED
@@ -65,6 +65,17 @@
|
|
65 |
<label class="btn btn-outline-primary" for="DIV">DIV</label>
|
66 |
</div>
|
67 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
</div>
|
69 |
</div>
|
70 |
</div>
|
@@ -473,7 +484,8 @@
|
|
473 |
items: getAllItems(),
|
474 |
frame_number: "",
|
475 |
status: "pending",
|
476 |
-
created_at: new Date().toISOString()
|
|
|
477 |
};
|
478 |
|
479 |
console.log("Sending invoice data:", data);
|
@@ -501,7 +513,7 @@
|
|
501 |
headers: {
|
502 |
"Content-Type": "application/json"
|
503 |
},
|
504 |
-
body: JSON.stringify(invoice)
|
505 |
});
|
506 |
|
507 |
if (!pdfResponse.ok) {
|
|
|
65 |
<label class="btn btn-outline-primary" for="DIV">DIV</label>
|
66 |
</div>
|
67 |
</div>
|
68 |
+
<div class="mb-3">
|
69 |
+
<label class="form-label">Commercial</label>
|
70 |
+
<select class="form-control" id="commercial" required>
|
71 |
+
<option value="">Sélectionner un commercial</option>
|
72 |
+
<option value="khaled">Khaled</option>
|
73 |
+
<option value="salah">Salah</option>
|
74 |
+
<option value="ismail">Ismail</option>
|
75 |
+
<option value="jamal">Jamal</option>
|
76 |
+
<option value="divers">Divers</option>
|
77 |
+
</select>
|
78 |
+
</div>
|
79 |
</div>
|
80 |
</div>
|
81 |
</div>
|
|
|
484 |
items: getAllItems(),
|
485 |
frame_number: "",
|
486 |
status: "pending",
|
487 |
+
created_at: new Date().toISOString(),
|
488 |
+
commercial: document.querySelector("#commercial").value || "divers"
|
489 |
};
|
490 |
|
491 |
console.log("Sending invoice data:", data);
|
|
|
513 |
headers: {
|
514 |
"Content-Type": "application/json"
|
515 |
},
|
516 |
+
body: JSON.stringify(invoice)
|
517 |
});
|
518 |
|
519 |
if (!pdfResponse.ok) {
|
sql_app.db
CHANGED
Binary files a/sql_app.db and b/sql_app.db differ
|
|