EL GHAFRAOUI AYOUB commited on
Commit
d749575
·
1 Parent(s): 7ede824
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:.2f}", 60),
169
- (f"{item.total_price:.2f}", 170)
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 just "NB:" text
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(60, current_y - nb_box_height + 60, "NB:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:.2f} DH"),
210
- ("TVA", "20 %", f"{data.tax:.2f} DH"),
211
- ("Total", "TTC", f"{data.total_ttc:.2f} DH")
212
  ]):
213
  y = current_y - (i * row_height)
214
- totals_x = (page_width - totals_table_width) - 27 # Center totals table
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
- footer_contact = "Tél: 05 24 01 55 54 Fax : 05 24 01 55 29 E-mail : [email protected]"
228
- pdf.drawCentredString(page_width / 2, 20, footer_contact)
 
 
 
 
 
 
 
 
 
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) // Send the created invoice data for PDF generation
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