- Delete /inventory/items/[id] — items expand inline in the list - Move SiteSelect from deleted [id] folder to components/inventory/site-select - Fix admin product detail page import to use new shared path - Fix items-table: Fragment key prop, restore Link import, plain text item names - Fix vendor-items-table: remove broken link to deleted item detail page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
349 lines
16 KiB
Python
349 lines
16 KiB
Python
"""
|
|
Generate PO output: XLSX (copy with computed values) + PDF (formatted).
|
|
Source: Prototype/Sample_PO.xlsx
|
|
Output: Progress/PMS_HNR3_056_2026-27.xlsx + Progress/PMS_HNR3_056_2026-27.pdf
|
|
"""
|
|
|
|
import shutil, os, datetime
|
|
import openpyxl
|
|
from openpyxl import load_workbook
|
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
# ── paths ────────────────────────────────────────────────────────────────────
|
|
BASE = r'C:\Users\shad0w\Documents\src\Peliagia_Portal'
|
|
SRC_XLSX = os.path.join(BASE, r'Prototype\Sample_PO.xlsx')
|
|
OUT_DIR = os.path.join(BASE, 'Progress')
|
|
OUT_XLSX = os.path.join(OUT_DIR, 'PMS_HNR3_056_2026-27.xlsx')
|
|
OUT_PDF = os.path.join(OUT_DIR, 'PMS_HNR3_056_2026-27.pdf')
|
|
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Computed values (resolved from the string-formula cells)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
QTY = 1050
|
|
UNIT_PRICE = 182
|
|
TAXABLE = QTY * UNIT_PRICE # 191,100
|
|
GST_RATE = 0.18
|
|
GST_AMT = round(TAXABLE * GST_RATE) # 34,398
|
|
GRAND_TOTAL= TAXABLE + GST_AMT # 225,498
|
|
PO_NO = 'PMS/HNR3/056/2026-27'
|
|
PO_DATE = datetime.date(2026, 4, 29)
|
|
VENDOR = 'Apar Industries Ltd'
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
# 1. XLSX — copy the template, replace text-formula cells with real formulas
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
shutil.copy2(SRC_XLSX, OUT_XLSX)
|
|
wb = load_workbook(OUT_XLSX)
|
|
ws = wb.active
|
|
|
|
# The source stores these as Excel formula strings — they're already valid
|
|
# formulas in the file; openpyxl reads them back as strings starting with '='.
|
|
# We re-write them so openpyxl treats them as formulas (no data_only quirk).
|
|
ws['G16'] = '=F16*E16'
|
|
ws['I16'] = '=G16+H16*G16'
|
|
ws['H24'] = '=SUM(G16:G22)'
|
|
ws['H25'] = '=H24*18%'
|
|
ws['H26'] = '=H24+H25'
|
|
ws['D26'] = '=SUM(D16:D20)'
|
|
ws['G39'] = '=C13'
|
|
|
|
# Format date cell properly
|
|
ws['I5'] = PO_DATE
|
|
ws['I5'].number_format = 'DD-MMM-YYYY'
|
|
|
|
wb.save(OUT_XLSX)
|
|
print(f'[XLSX] Saved: {OUT_XLSX}')
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
# 2. PDF — render with reportlab
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.units import mm
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.styles import ParagraphStyle
|
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
|
from reportlab.platypus import (
|
|
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
|
|
)
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
|
|
W, H = A4
|
|
MARGIN = 12 * mm
|
|
|
|
def mk_style(name, **kw):
|
|
return ParagraphStyle(name, **kw)
|
|
|
|
HEAD = mk_style('Head', fontName='Helvetica-Bold', fontSize=14, alignment=TA_CENTER, spaceAfter=1)
|
|
SUB = mk_style('Sub', fontName='Helvetica', fontSize=8, alignment=TA_CENTER, spaceAfter=1)
|
|
TITLE = mk_style('Title', fontName='Helvetica-Bold', fontSize=12, alignment=TA_CENTER, spaceAfter=4)
|
|
LABEL = mk_style('Label', fontName='Helvetica-Bold', fontSize=7.5, alignment=TA_LEFT)
|
|
VALUE = mk_style('Value', fontName='Helvetica', fontSize=7.5, alignment=TA_LEFT)
|
|
VALUEC = mk_style('ValueC', fontName='Helvetica', fontSize=7.5, alignment=TA_CENTER)
|
|
VALUER = mk_style('ValueR', fontName='Helvetica', fontSize=7.5, alignment=TA_RIGHT)
|
|
BOLDVAL = mk_style('BoldV', fontName='Helvetica-Bold', fontSize=7.5, alignment=TA_CENTER)
|
|
INSTRH = mk_style('InstrH', fontName='Helvetica-Bold', fontSize=7.5, alignment=TA_CENTER)
|
|
|
|
BLK = colors.black
|
|
GRAY = colors.HexColor('#D0D0D0')
|
|
LGRY = colors.HexColor('#F0F0F0')
|
|
|
|
def thin_box():
|
|
return [
|
|
('BOX', (0, 0), (-1, -1), 0.5, BLK),
|
|
('INNERGRID', (0, 0), (-1, -1), 0.3, BLK),
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 7.5),
|
|
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 3),
|
|
('RIGHTPADDING',(0, 0), (-1, -1), 3),
|
|
('TOPPADDING', (0, 0), (-1, -1), 2),
|
|
('BOTTOMPADDING',(0, 0), (-1, -1), 2),
|
|
]
|
|
|
|
story = []
|
|
TW = W - 2 * MARGIN # total usable width
|
|
|
|
# ── Header ───────────────────────────────────────────────────────────────────
|
|
story.append(Paragraph('PELAGIA MARINE SERVICES PVT. LTD', HEAD))
|
|
story.append(Paragraph(
|
|
'Office address: 409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210', SUB))
|
|
story.append(Paragraph(
|
|
'Tel: +91-22-6909 9028 / Email: technical@pelagiamarine.com / Mob: +91 74000 60772', SUB))
|
|
story.append(HRFlowable(width=TW, thickness=1, color=BLK, spaceAfter=3))
|
|
story.append(Paragraph('PURCHASE ORDER', TITLE))
|
|
story.append(HRFlowable(width=TW, thickness=1, color=BLK, spaceAfter=3))
|
|
|
|
# ── PO meta table ────────────────────────────────────────────────────────────
|
|
po_date_str = PO_DATE.strftime('%d-%b-%Y')
|
|
|
|
meta_data = [
|
|
[
|
|
Paragraph('<b>Purchase Order No:</b>', LABEL),
|
|
Paragraph(PO_NO, BOLDVAL),
|
|
Paragraph('<b>Date:</b>', LABEL),
|
|
Paragraph(po_date_str, VALUEC),
|
|
],
|
|
[
|
|
Paragraph('<b>Performa Invoice / Quotation No:</b>', LABEL),
|
|
Paragraph('Verbal', VALUEC),
|
|
Paragraph('<b>P I / Quotation Date:</b>', LABEL),
|
|
Paragraph('', VALUEC),
|
|
],
|
|
]
|
|
col_w = [TW * 0.32, TW * 0.24, TW * 0.22, TW * 0.22]
|
|
meta_tbl = Table(meta_data, colWidths=col_w, repeatRows=0)
|
|
meta_tbl.setStyle(TableStyle(thin_box() + [
|
|
('FONTNAME', (1, 0), (1, 0), 'Helvetica-Bold'),
|
|
]))
|
|
story.append(meta_tbl)
|
|
|
|
# ── Vessel / Requisition / Approved ──────────────────────────────────────────
|
|
vessel_data = [
|
|
[
|
|
Paragraph('<b>Vessel Owner Name</b>', LABEL),
|
|
Paragraph('Pelagia Marine Services Pvt. Ltd.', VALUE),
|
|
Paragraph('<b>Budget head</b>', LABEL),
|
|
Paragraph('700203', VALUEC),
|
|
Paragraph('<b>Requested By</b>', LABEL),
|
|
Paragraph('Kaushal Pal Singh', VALUE),
|
|
],
|
|
[
|
|
Paragraph('<b>Vessel/Office Requisition No.</b>', LABEL),
|
|
Paragraph('', VALUE),
|
|
Paragraph('<b>Reqn. Date</b>', LABEL),
|
|
Paragraph('', VALUEC),
|
|
Paragraph('<b>Approved By</b>', LABEL),
|
|
Paragraph('Kaushal Pal Singh', VALUE),
|
|
],
|
|
]
|
|
v_w = [TW*0.20, TW*0.17, TW*0.12, TW*0.10, TW*0.14, TW*0.27]
|
|
vessel_tbl = Table(vessel_data, colWidths=v_w)
|
|
vessel_tbl.setStyle(TableStyle(thin_box()))
|
|
story.append(vessel_tbl)
|
|
|
|
# ── Place of Delivery + Invoice Details ──────────────────────────────────────
|
|
delivery_data = [
|
|
[
|
|
Paragraph('<b>Place of Delivery</b>', LABEL),
|
|
Paragraph(
|
|
'Pelagia Marine Services Pvt. Ltd. Reti Bundar Near Konkan Bhavan, '
|
|
'CBD Belapur, Navi Mumbai - 400614', VALUE),
|
|
],
|
|
[
|
|
Paragraph('<b>Invoice Details</b>', LABEL),
|
|
Paragraph(
|
|
'Pelagia Marine Services Pvt Ltd, 409-410, ZION, Plot 273, Sector-10, '
|
|
'Kharghar, Navi Mumbai- 410210 (MH)<br/>'
|
|
'Email: accounts@pelagiamarine.com GST NO: 27AAHCP5787B1Z6', VALUE),
|
|
],
|
|
]
|
|
d_w = [TW*0.22, TW*0.78]
|
|
delivery_tbl = Table(delivery_data, colWidths=d_w)
|
|
delivery_tbl.setStyle(TableStyle(thin_box()))
|
|
story.append(delivery_tbl)
|
|
|
|
# ── Vendor ────────────────────────────────────────────────────────────────────
|
|
vendor_data = [
|
|
[
|
|
Paragraph('<b>Vendor Name & Address</b>', LABEL),
|
|
Paragraph(VENDOR, VALUE),
|
|
Paragraph(
|
|
'18, TTC MIDC Industrial Area Thane Belapur Road, Opp Rabale Railway Stn '
|
|
'Rabale, Navi Mumbai 400701 GSTIN: 27AAACG1840M1ZL', VALUE),
|
|
],
|
|
[
|
|
Paragraph('<b>Contact Person / Mobile</b>', LABEL),
|
|
Paragraph(
|
|
'Mr. Nikhil Mumbaikar Ph. 7208055636 '
|
|
'Email: nikhil.mumbaikar@apar.com', VALUE),
|
|
Paragraph('', VALUE),
|
|
],
|
|
]
|
|
vd_w = [TW*0.22, TW*0.22, TW*0.56]
|
|
vendor_tbl = Table(vendor_data, colWidths=vd_w)
|
|
vendor_tbl.setStyle(TableStyle(thin_box()))
|
|
story.append(vendor_tbl)
|
|
|
|
# ── Line items ────────────────────────────────────────────────────────────────
|
|
items_header = [
|
|
Paragraph('<b>S.N.</b>', BOLDVAL),
|
|
Paragraph('<b>Description</b>', BOLDVAL),
|
|
Paragraph('<b>Unit</b>', BOLDVAL),
|
|
Paragraph('<b>Qty</b>', BOLDVAL),
|
|
Paragraph('<b>Unit Price</b>', BOLDVAL),
|
|
Paragraph('<b>Taxable Cost</b>', BOLDVAL),
|
|
Paragraph('<b>GST %</b>', BOLDVAL),
|
|
Paragraph('<b>Total Cost</b>', BOLDVAL),
|
|
]
|
|
items_row = [
|
|
Paragraph('1', VALUEC),
|
|
Paragraph('Eni EP 80W90 GEAR OIL', VALUE),
|
|
Paragraph('Ltr', VALUEC),
|
|
Paragraph(f'{QTY:,}', VALUEC),
|
|
Paragraph(f'{UNIT_PRICE:,.2f}', VALUER),
|
|
Paragraph(f'{TAXABLE:,.2f}', VALUER),
|
|
Paragraph('18%', VALUEC),
|
|
Paragraph(f'{GRAND_TOTAL:,.2f}', VALUER),
|
|
]
|
|
# blank filler rows
|
|
blank = [''] * 8
|
|
items_data = [items_header, items_row] + [blank] * 6
|
|
|
|
i_w = [TW*0.05, TW*0.29, TW*0.07, TW*0.07, TW*0.10, TW*0.14, TW*0.08, TW*0.20]
|
|
items_tbl = Table(items_data, colWidths=i_w, rowHeights=[None] + [10*mm]*7)
|
|
items_tbl.setStyle(TableStyle(thin_box() + [
|
|
('BACKGROUND', (0, 0), (-1, 0), LGRY),
|
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
]))
|
|
story.append(items_tbl)
|
|
|
|
# ── Totals ────────────────────────────────────────────────────────────────────
|
|
totals_data = [
|
|
['', '', '', '', '', Paragraph('<b>Total Taxable Value</b>', LABEL), '', Paragraph(f'{TAXABLE:,.2f}', VALUER)],
|
|
['', '', '', '', '', Paragraph('<b>GST (18%)</b>', LABEL), '', Paragraph(f'{GST_AMT:,.2f}', VALUER)],
|
|
[
|
|
Paragraph(f'Total Qty: {QTY:,} Ltr', VALUE),
|
|
'', '', '',
|
|
'',
|
|
Paragraph('<b>GRAND TOTAL</b>', LABEL),
|
|
'',
|
|
Paragraph(f'<b>{GRAND_TOTAL:,.2f}</b>', mk_style('GT', fontName='Helvetica-Bold', fontSize=7.5, alignment=TA_RIGHT)),
|
|
],
|
|
]
|
|
totals_tbl = Table(totals_data, colWidths=i_w)
|
|
totals_tbl.setStyle(TableStyle([
|
|
('BOX', (0, 0), (-1, -1), 0.5, BLK),
|
|
('INNERGRID', (0, 0), (-1, -1), 0.3, BLK),
|
|
('SPAN', (0, 2), (3, 2)), # total qty spans cols 0-3
|
|
('SPAN', (5, 0), (6, 0)), # label spans 5-6 row 0
|
|
('SPAN', (5, 1), (6, 1)),
|
|
('SPAN', (5, 2), (6, 2)),
|
|
('ALIGN', (7, 0), (7, 2), 'RIGHT'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 7.5),
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 3),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 3),
|
|
('TOPPADDING', (0, 0), (-1, -1), 2),
|
|
('BOTTOMPADDING',(0, 0), (-1, -1), 2),
|
|
('BACKGROUND', (5, 2), (7, 2), LGRY),
|
|
]))
|
|
story.append(totals_tbl)
|
|
|
|
# ── Instructions to Vendors ───────────────────────────────────────────────────
|
|
story.append(Spacer(1, 3*mm))
|
|
instr_header = [[Paragraph('<b>INSTRUCTIONS TO VENDORS</b>', INSTRH)]]
|
|
instr_tbl_h = Table(instr_header, colWidths=[TW])
|
|
instr_tbl_h.setStyle(TableStyle([
|
|
('BOX', (0, 0), (-1, -1), 0.5, BLK),
|
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 7.5),
|
|
('TOPPADDING', (0, 0), (-1, -1), 2),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
|
|
]))
|
|
story.append(instr_tbl_h)
|
|
|
|
instructions = [
|
|
(1, 'Please quote this purchase order no. for further communications and invoices pertaining to this indent.'),
|
|
(2, 'DELIVERY: Within 4 to 5 days'),
|
|
(3, "DISPATCH INSTRUCTIONS: To be transported to Navi Mumbai Site Address as above. Freight Supplier's A/C"),
|
|
(4, 'INSPECTION: NA'),
|
|
(5, 'TRANSIT INSURANCE: NA'),
|
|
(6, 'PAYMENT TERMS: Within 30 days from delivery.'),
|
|
(7, 'We encourage bulk packaging and avoid plastic. No asbestos to be used in any product or packing material.'),
|
|
]
|
|
instr_data = [[Paragraph(str(n), VALUEC), Paragraph(txt, VALUE)] for n, txt in instructions]
|
|
instr_tbl = Table(instr_data, colWidths=[TW*0.05, TW*0.95])
|
|
instr_tbl.setStyle(TableStyle([
|
|
('BOX', (0, 0), (-1, -1), 0.5, BLK),
|
|
('INNERGRID',(0, 0), (-1, -1), 0.3, BLK),
|
|
('FONTSIZE', (0, 0), (-1, -1), 7.5),
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 3),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 3),
|
|
('TOPPADDING', (0, 0), (-1, -1), 2),
|
|
('BOTTOMPADDING',(0, 0), (-1, -1), 2),
|
|
]))
|
|
story.append(instr_tbl)
|
|
|
|
# ── Signature block ───────────────────────────────────────────────────────────
|
|
story.append(Spacer(1, 6*mm))
|
|
sig_data = [
|
|
[
|
|
Paragraph('Kaushal Pal Singh', mk_style('SN', fontName='Helvetica', fontSize=7.5, alignment=TA_CENTER)),
|
|
'',
|
|
Paragraph(VENDOR, mk_style('SV', fontName='Helvetica-Bold', fontSize=7.5, alignment=TA_CENTER)),
|
|
],
|
|
[
|
|
Paragraph('Authorized Signatory & Stamp', mk_style('SA', fontName='Helvetica', fontSize=7, alignment=TA_CENTER)),
|
|
'',
|
|
Paragraph('Authorized Signatory & Stamp', mk_style('SB', fontName='Helvetica', fontSize=7, alignment=TA_CENTER)),
|
|
],
|
|
[
|
|
Paragraph('For, Pelagia Marine Services Pvt. Ltd.', mk_style('SF', fontName='Helvetica', fontSize=7.5, alignment=TA_CENTER)),
|
|
'',
|
|
Paragraph(f'For, {VENDOR}', mk_style('SFF', fontName='Helvetica', fontSize=7.5, alignment=TA_CENTER)),
|
|
],
|
|
]
|
|
sig_tbl = Table(sig_data, colWidths=[TW*0.45, TW*0.10, TW*0.45])
|
|
sig_tbl.setStyle(TableStyle([
|
|
('BOX', (0, 0), (0, -1), 0.5, BLK),
|
|
('BOX', (2, 0), (2, -1), 0.5, BLK),
|
|
('FONTSIZE', (0, 0), (-1, -1), 7.5),
|
|
('TOPPADDING', (0, 0), (-1, -1), 2),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
|
|
]))
|
|
story.append(sig_tbl)
|
|
|
|
# ── Build PDF ─────────────────────────────────────────────────────────────────
|
|
doc = SimpleDocTemplate(
|
|
OUT_PDF,
|
|
pagesize=A4,
|
|
leftMargin=MARGIN, rightMargin=MARGIN,
|
|
topMargin=MARGIN, bottomMargin=MARGIN,
|
|
)
|
|
doc.build(story)
|
|
print(f'[PDF] Saved: {OUT_PDF}')
|