edf-tool 0.1.0__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.
edf_tool/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """edf-tool: monthly export-of-services invoice + bank EDF form generator. Runs fully offline."""
2
+
3
+ __version__ = "0.1.0"
edf_tool/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,22 @@
1
+ """Bank form adapters.
2
+
3
+ Each adapter module exposes:
4
+ NAME, DESCRIPTION, TEMPLATE (file in edf_tool/data), PDF_SHEETS (sheets in the 2-page PDF)
5
+ check_template(wb) -> raise TemplateError if the file isn't the layout we know
6
+ fill(wb, cfg, inv) -> write all values into the workbook
7
+ place_signature(wb, png) -> float the signature image over the signatory line
8
+
9
+ To support another bank, copy hdfc_services.py, change the cell map, and register it below.
10
+ See CONTRIBUTING.md.
11
+ """
12
+ from ..errors import ConfigError
13
+ from . import hdfc_services
14
+
15
+ REGISTRY = {hdfc_services.NAME: hdfc_services}
16
+
17
+
18
+ def get(name):
19
+ try:
20
+ return REGISTRY[name]
21
+ except KeyError:
22
+ raise ConfigError("Unknown bank_format '%s'. Supported: %s" % (name, ", ".join(sorted(REGISTRY))))
@@ -0,0 +1,138 @@
1
+ """HDFC Bank - "Request letter for Export of Services & EDF Filing Cum Disposal Instructions
2
+ for Credit" (the version HDFC circulated in September 2026).
3
+
4
+ Only this exact layout has been tested. If HDFC changes the form, check_template() stops
5
+ the run instead of writing values into the wrong cells.
6
+ """
7
+ from ..errors import TemplateError
8
+ from ..textutil import amount_in_words, dmony, dmy, money, num, short_money
9
+
10
+ NAME = "hdfc_services"
11
+ DESCRIPTION = "HDFC Bank - Export of Services EDF request letter + annexure (Sept 2026 form)"
12
+ TEMPLATE = "hdfc_services_2026_09.xlsx"
13
+
14
+ LETTER = "REQUEST LETTER"
15
+ ANNEX = "EDF Annexure"
16
+ INV1 = "Invoice Details 1 " # trailing space is in HDFC's file
17
+ INV2 = "Invoice Details 2 "
18
+ PDF_SHEETS = (LETTER, INV1)
19
+
20
+ # (sheet, cell, text the label must start with)
21
+ FINGERPRINT = [
22
+ (LETTER, "A1", "Request letter for Export of Services"),
23
+ (LETTER, "A5", "Account no. to be Credited"),
24
+ (LETTER, "A6", "Purpose Code"),
25
+ (LETTER, "A11", "Exporter Name"),
26
+ (LETTER, "A19", "Description of Services"),
27
+ (LETTER, "A21", "Third Party"),
28
+ (LETTER, "J24", "Invoice No"),
29
+ (LETTER, "A29", "Debit all processing charges"),
30
+ (LETTER, "A34", "Remark"),
31
+ (ANNEX, "A9", "EDF Form No"),
32
+ (ANNEX, "K9", "Service recipient Name"),
33
+ (ANNEX, "T9", "Relationship between Exporter"),
34
+ (INV1, "A2", "Invoice No"),
35
+ (INV1, "J2", "Remarks"),
36
+ ]
37
+
38
+
39
+ def check_template(wb):
40
+ bad = []
41
+ for sheet, ref, expect in FINGERPRINT:
42
+ got = (wb.value(sheet, ref) or "").strip()
43
+ if not got.lower().startswith(expect.lower()):
44
+ bad.append("%s!%s expected '%s...' but found '%s'" % (sheet, ref, expect, got[:40]))
45
+ if bad:
46
+ raise TemplateError("This doesn't look like the HDFC export-of-services form edf-tool knows:\n "
47
+ + "\n ".join(bad),
48
+ hint="HDFC may have changed the form. Open an issue with the new form attached "
49
+ "(remove your personal data first).")
50
+
51
+
52
+ def fill(wb, c, inv):
53
+ ex, bk, cl, tp, sv = c["exporter"], c["bank"], c["client"], c["third_party"], c["service"]
54
+ cur = sv["currency"]
55
+ amt, net, fee = inv["amount"], inv["net"], inv["fee"]
56
+ has_tp = bool(tp.get("enabled"))
57
+ payer_short = tp.get("name", "").split(",")[0] if has_tp else ""
58
+
59
+ words = "%s (%s %s)" % (amount_in_words(amt, cur), cur, money(amt))
60
+ exporter = "M/S. %s (%s: %s), %s" % (ex["firm_name"], "Proprietor" if "propriet" in ex.get("constitution", "").lower()
61
+ else "Authorised signatory", ex["proprietor"], ", ".join(ex["address_lines"]))
62
+ via = ("by SWIFT wire from %s on behalf of %s" % (tp["name"], cl["name"])) if has_tp else ("by SWIFT wire from %s" % cl["name"])
63
+ fee_txt = ("; %s deducts %s %s transaction fee, net %s %s" % (payer_short or "Intermediary", cur, short_money(fee), cur,
64
+ short_money(net))) if fee else ""
65
+ remark = "Payment for invoice %s (%s %s) will be received %s%s." % (inv["invoice_no"], cur, short_money(amt), via, fee_txt)
66
+ if fee:
67
+ inv_remark = ("Via %s; %s %s fee deducted, net %s %s" % (payer_short, cur, short_money(fee), cur, short_money(net))
68
+ if has_tp else "%s %s charges deducted, net %s %s" % (cur, short_money(fee), cur, short_money(net)))
69
+ row_remark = ("%s fee %s %s" % (payer_short, cur, short_money(fee))) if has_tp else ("Charges %s %s" % (cur, short_money(fee)))
70
+ else:
71
+ inv_remark = ("Via %s" % payer_short) if has_tp else "-"
72
+ row_remark = ("Via %s" % payer_short) if has_tp else "-"
73
+ signer_role = "Proprietor" if "propriet" in ex.get("constitution", "").lower() else "Authorised Signatory"
74
+
75
+ L = LETTER
76
+ for ref, v in [
77
+ ("A4", bk["branch_name"]), ("AG3", dmony(inv["letter_date"])), ("H5", bk["account_no"]),
78
+ ("F6", "%s - %s" % (sv["purpose_code"], sv["purpose_description"])),
79
+ ("L7", "No forward contract - bank card rate on date of credit"),
80
+ ("A8", "AD Code: To be filled by Bank"), ("P8", "IEC: %s" % (ex.get("iec") or "Not applicable")),
81
+ ("AB8", "GSTIN: %s" % ex["gstin"]), ("A9", "Customer PAN: %s" % ex["pan"]),
82
+ ("P10", "%s, %s, %s (IFSC: %s)" % (bk.get("name", "HDFC Bank Ltd"), bk["branch_name"], bk["branch_address"], bk["ifsc"])),
83
+ ("P11", exporter),
84
+ ("E12", "Service"), ("AD12", "Regular Export"), ("F13", "Internet"),
85
+ ("P14", "Others (Specify) - Service exporter"),
86
+ ("AD15", "Country of Final Destination: %s" % cl["country"]),
87
+ ("D16", dmy(inv["invoice_date"])), ("U16", "Non dispatch"),
88
+ ("G19", sv["description"]), ("G20", words),
89
+ ("E21", "YES" if has_tp else "NO"),
90
+ ("S21", ("%s, %s" % (tp["name"], tp.get("short_address") or tp["address"])) if has_tp else "-"),
91
+ ("B25", cl.get("short_address") or cl["name"]), ("E25", cl["country"]), ("J25", inv["invoice_no"]),
92
+ ("O25", dmy(inv["invoice_date"])), ("T25", cur), ("U25", num(amt)), ("X25", num(net)), ("AB25", "-"),
93
+ ("AE25", sv.get("short_description") or sv["description"]), ("AH25", sv["sac"]), ("AK25", row_remark),
94
+ ("Q29", bk.get("fee_account_no") or bk["account_no"]), ("M30", ex["phone"]), ("AE30", ex["email"]),
95
+ ("E34", remark), ("B35", "%s (%s: %s)" % (ex["firm_name"], signer_role, ex["proprietor"])),
96
+ ("B40", "AUTHORISED SIGNATORY\n(Signature as per Bank record with company seal)"),
97
+ ]:
98
+ wb.set(L, ref, v)
99
+ wb.restyle(L, "G19", "113", "114") # HDFC's template has a strikethrough font on this cell
100
+
101
+ A = ANNEX
102
+ wb.set(A, "A6", bk.get("fee_account_no") or bk["account_no"])
103
+ for ref in ("B7", "L7", "O7"):
104
+ wb.set(A, ref, None) # stray test text in the template
105
+ for ref, v in [
106
+ ("B11", "Service"), ("C11", "Internet"), ("D11", "Others (Specify)"),
107
+ ("E11", "Others (advance payment, etc. including transfer/remittance to bank a/c maintained overseas)"),
108
+ ("F11", "Regular Export"), ("H11", "Non dispatch"), ("I11", sv["description"]), ("J11", words),
109
+ ("K11", cl["name"]), ("L11", ", ".join(cl["address_lines"])), ("M11", cl["country"]),
110
+ ("Q11", tp["name"] if has_tp else None), ("R11", tp["address"] if has_tp else None),
111
+ ("S11", cl["country"]), ("T11", tp.get("relationship") if has_tp else None),
112
+ ("U11", "Service exporter - %s (non-SEZ/EOU/STPI)" % ex.get("constitution", "Sole Proprietorship")),
113
+ ]:
114
+ wb.set(A, ref, v)
115
+ for col in "BCDEFHIJ":
116
+ wb.set(A, col + "12", None) # one EDF per run
117
+
118
+ I = INV1
119
+ for ref, v in [("A3", inv["invoice_no"]), ("B3", dmy(inv["invoice_date"])), ("C3", num(amt)), ("D3", cur),
120
+ ("E3", num(net)), ("F3", "-"), ("G3", "-"), ("H3", sv["description"]), ("I3", sv["sac"]),
121
+ ("J3", inv_remark)]:
122
+ wb.set(I, ref, v)
123
+ for r in range(4, 8):
124
+ wb.set(I, "A%d" % r, None) # template placeholder numbers
125
+ for r in range(3, 8):
126
+ wb.set(INV2, "A%d" % r, None)
127
+
128
+ heights = {r: 22 for r in list(range(2, 10)) + [14, 19, 20, 21, 29, 30, 33, 35, 39]}
129
+ heights.update({36: 30, 37: 30, 38: 30, 10: 34, 11: 34, 12: 30, 13: 30, 16: 22, 17: 22, 18: 22,
130
+ 25: 36, 26: 20, 27: 20, 28: 20, 34: 40})
131
+ wb.layout(L, heights)
132
+ wb.layout(A, {11: 110}, landscape=True, pages_wide=3)
133
+ wb.layout(I, {3: 60}, landscape=True)
134
+
135
+
136
+ def place_signature(wb, png):
137
+ # just above the "____" signatory line (B39), left aligned with it
138
+ wb.add_image(LETTER, png, col=1, row=36, col_off=60000, row_off=260000, width_emu=1950000)