Skip to content

Commit 118f19e

Browse files
authored
invoices
invoices
2 parents 3013c20 + f0acc52 commit 118f19e

File tree

111 files changed

+20440
-1794
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+20440
-1794
lines changed

backend/README.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,41 @@ source venv/bin/activate # On Windows: venv\Scripts\activate
5656
pip install -r requirements.txt
5757
```
5858

59-
### 3. Configure environment variables
59+
### 3. Install PDF generation system dependencies
60+
61+
Invoice PDF generation uses WeasyPrint (already included in requirements.txt). However, WeasyPrint requires system libraries that must be installed separately:
62+
63+
**Ubuntu/Debian:**
64+
```bash
65+
sudo apt-get install -y \
66+
libpango-1.0-0 \
67+
libpangocairo-1.0-0 \
68+
libcairo2 \
69+
libgdk-pixbuf2.0-0 \
70+
libffi-dev \
71+
shared-mime-info
72+
```
73+
74+
**macOS:**
75+
```bash
76+
brew install pango cairo libffi gdk-pixbuf
77+
```
78+
79+
**Fedora/CentOS:**
80+
```bash
81+
sudo dnf install -y \
82+
pango \
83+
cairo \
84+
gdk-pixbuf2 \
85+
libffi-devel
86+
```
87+
88+
**Windows:**
89+
Follow the [WeasyPrint Windows installation guide](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#windows).
90+
91+
> **Note**: If you skip this step, the CRM will work but PDF download for invoices will show "PDF generation unavailable".
92+
93+
### 4. Configure environment variables
6094

6195
Create a `.env` file in the `backend/` directory:
6296

@@ -85,7 +119,7 @@ DOMAIN_NAME=http://localhost:8000
85119
SWAGGER_ROOT_URL=http://localhost:8000
86120
```
87121

88-
### 4. Set up database
122+
### 5. Set up database
89123

90124
```bash
91125
# Create PostgreSQL database
@@ -101,7 +135,7 @@ python manage.py migrate
101135
python manage.py createsuperuser
102136
```
103137

104-
### 5. Run the development server
138+
### 6. Run the development server
105139

106140
```bash
107141
python manage.py runserver

backend/accounts/views.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
)
4646
from contacts.models import Contact
4747
from contacts.serializer import ContactSerializer
48-
from invoices.serializer import InvoiceSerailizer
48+
from invoices.serializer import InvoiceListSerializer
4949
from leads.models import Lead
5050
from leads.serializer import LeadSerializer
5151
from opportunity.models import SOURCES, STAGES, Opportunity
@@ -145,7 +145,7 @@ def get_context_data(self, **kwargs):
145145
context["countries"] = COUNTRIES
146146
context["industries"] = INDCHOICES
147147

148-
tags = Tags.objects.filter(org=self.request.profile.org)
148+
tags = Tags.objects.filter(org=self.request.profile.org, is_active=True)
149149
tags = TagsSerializer(tags, many=True).data
150150

151151
context["tags"] = tags
@@ -286,13 +286,8 @@ def put(self, request, pk, format=None):
286286
account_object.tags.clear()
287287
if data.get("tags"):
288288
tags = json.loads(data.get("tags"))
289-
for tag in tags:
290-
tag_obj = Tags.objects.filter(slug=tag.lower(), org=request.profile.org)
291-
if tag_obj.exists():
292-
tag_obj = tag_obj[0]
293-
else:
294-
tag_obj = Tags.objects.create(name=tag, org=request.profile.org)
295-
account_object.tags.add(tag_obj)
289+
tag_objs = Tags.objects.filter(id__in=tags, org=request.profile.org, is_active=True)
290+
account_object.tags.add(*tag_objs)
296291

297292
account_object.teams.clear()
298293
if data.get("teams"):
@@ -459,8 +454,8 @@ def get(self, request, pk, format=None):
459454
"tasks": TaskSerializer(
460455
self.account.accounts_tasks.all(), many=True
461456
).data,
462-
"invoices": InvoiceSerailizer(
463-
self.account.accounts_invoices.all(), many=True
457+
"invoices": InvoiceListSerializer(
458+
self.account.invoices.all(), many=True
464459
).data,
465460
"emails": EmailSerializer(
466461
self.account.sent_email.all(), many=True
@@ -596,13 +591,8 @@ def patch(self, request, pk, format=None):
596591
if tags:
597592
if isinstance(tags, str):
598593
tags = json.loads(tags)
599-
for tag in tags:
600-
tag_obj = Tags.objects.filter(slug=tag.lower(), org=request.profile.org)
601-
if tag_obj.exists():
602-
tag_obj = tag_obj[0]
603-
else:
604-
tag_obj = Tags.objects.create(name=tag, org=request.profile.org)
605-
account_object.tags.add(tag_obj)
594+
tag_objs = Tags.objects.filter(id__in=tags, org=request.profile.org, is_active=True)
595+
account_object.tags.add(*tag_objs)
606596

607597
if "teams" in data:
608598
account_object.teams.clear()

backend/cases/views.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,8 @@ def post(self, request, *args, **kwargs):
180180
tags = params.get("tags")
181181
if isinstance(tags, str):
182182
tags = json.loads(tags)
183-
for tag in tags:
184-
tag_obj = Tags.objects.filter(slug=tag.lower(), org=request.profile.org)
185-
if tag_obj.exists():
186-
tag_obj = tag_obj[0]
187-
else:
188-
tag_obj = Tags.objects.create(name=tag, org=request.profile.org)
189-
cases_obj.tags.add(tag_obj)
183+
tag_objs = Tags.objects.filter(id__in=tags, org=request.profile.org, is_active=True)
184+
cases_obj.tags.add(*tag_objs)
190185

191186
if self.request.FILES.get("case_attachment"):
192187
attachment = Attachments()
@@ -300,13 +295,8 @@ def put(self, request, pk, format=None):
300295
tags = params.get("tags")
301296
if isinstance(tags, str):
302297
tags = json.loads(tags)
303-
for tag in tags:
304-
tag_obj = Tags.objects.filter(slug=tag.lower(), org=request.profile.org)
305-
if tag_obj.exists():
306-
tag_obj = tag_obj[0]
307-
else:
308-
tag_obj = Tags.objects.create(name=tag, org=request.profile.org)
309-
cases_object.tags.add(tag_obj)
298+
tag_objs = Tags.objects.filter(id__in=tags, org=request.profile.org, is_active=True)
299+
cases_object.tags.add(*tag_objs)
310300

311301
if self.request.FILES.get("case_attachment"):
312302
attachment = Attachments()
@@ -609,13 +599,8 @@ def patch(self, request, pk, format=None):
609599
if tags_list:
610600
if isinstance(tags_list, str):
611601
tags_list = json.loads(tags_list)
612-
for tag in tags_list:
613-
tag_obj = Tags.objects.filter(slug=tag.lower(), org=request.profile.org)
614-
if tag_obj.exists():
615-
tag_obj = tag_obj[0]
616-
else:
617-
tag_obj = Tags.objects.create(name=tag, org=request.profile.org)
618-
cases_object.tags.add(tag_obj)
602+
tag_objs = Tags.objects.filter(id__in=tags_list, org=request.profile.org, is_active=True)
603+
cases_object.tags.add(*tag_objs)
619604

620605
return Response(
621606
{"error": False, "message": "Case Updated Successfully"},

backend/common/management/commands/seed_data.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
STAGES,
3333
STATUS_CHOICE,
3434
)
35+
from invoices.seed import InvoiceSeeder
3536

3637

3738
class Command(BaseCommand):
@@ -99,6 +100,7 @@ def __init__(self, *args, **kwargs):
99100
super().__init__(*args, **kwargs)
100101
self.fake = None
101102
self.admin_user = None
103+
self.invoice_seeder = None
102104
self.stats = {
103105
"orgs": 0,
104106
"users": 0,
@@ -184,6 +186,38 @@ def add_arguments(self, parser):
184186
help="Tags per organization (default: 5)",
185187
)
186188

189+
# Invoice-related arguments
190+
parser.add_argument(
191+
"--products",
192+
type=int,
193+
default=20,
194+
help="Products per organization (default: 20)",
195+
)
196+
parser.add_argument(
197+
"--invoices",
198+
type=int,
199+
default=50,
200+
help="Invoices per organization (default: 50)",
201+
)
202+
parser.add_argument(
203+
"--estimates",
204+
type=int,
205+
default=15,
206+
help="Estimates per organization (default: 15)",
207+
)
208+
parser.add_argument(
209+
"--recurring-invoices",
210+
type=int,
211+
default=5,
212+
help="Recurring invoices per organization (default: 5)",
213+
)
214+
parser.add_argument(
215+
"--invoice-templates",
216+
type=int,
217+
default=3,
218+
help="Invoice templates per organization (default: 3)",
219+
)
220+
187221
# Options
188222
parser.add_argument(
189223
"--seed",
@@ -217,6 +251,9 @@ def handle(self, *args, **options):
217251
else:
218252
self.fake = Faker(["en_US", "en_GB", "en_CA", "en_AU"])
219253

254+
# Initialize InvoiceSeeder
255+
self.invoice_seeder = InvoiceSeeder(self.fake, self.stdout)
256+
220257
self.stdout.write(self.style.MIGRATE_HEADING("Seeding CRM database..."))
221258
if seed:
222259
self.stdout.write(f"Using seed: {seed}")
@@ -264,6 +301,9 @@ def clear_data(self):
264301
from opportunity.models import Opportunity
265302
from tasks.models import Task
266303

304+
# Clear invoice data first (depends on accounts/contacts)
305+
self.invoice_seeder.clear_invoice_data()
306+
267307
# Delete in reverse dependency order
268308
Task.objects.all().delete()
269309
Case.objects.all().delete()
@@ -310,12 +350,34 @@ def seed_all(self, options):
310350
accounts = self.create_accounts(
311351
org, profiles, teams, tags, contacts, options["accounts"]
312352
)
353+
354+
# Invoice prerequisites
355+
products = self.invoice_seeder.create_products(org, options["products"])
356+
templates = self.invoice_seeder.create_invoice_templates(
357+
org, options["invoice_templates"]
358+
)
359+
313360
leads = self.create_leads(
314361
org, profiles, teams, tags, contacts, options["leads"]
315362
)
316363
opportunities = self.create_opportunities(
317364
org, profiles, teams, tags, contacts, accounts, options["opportunities"]
318365
)
366+
367+
# Invoice entities
368+
invoices = self.invoice_seeder.create_invoices(
369+
org, profiles, teams, products, templates,
370+
accounts, contacts, opportunities, options["invoices"]
371+
)
372+
self.invoice_seeder.create_payments(org, invoices)
373+
self.invoice_seeder.create_estimates(
374+
org, profiles, teams, products, accounts, contacts, options["estimates"]
375+
)
376+
self.invoice_seeder.create_recurring_invoices(
377+
org, profiles, teams, products, accounts, contacts,
378+
options["recurring_invoices"]
379+
)
380+
319381
cases = self.create_cases(
320382
org, profiles, teams, tags, contacts, accounts, options["cases"]
321383
)
@@ -771,6 +833,9 @@ def _stage_to_probability(self, stage):
771833

772834
def print_summary(self, elapsed_seconds):
773835
"""Print seeding summary."""
836+
# Merge invoice stats
837+
self.stats.update(self.invoice_seeder.stats)
838+
774839
self.stdout.write("")
775840
self.stdout.write(self.style.SUCCESS("Seeding complete!"))
776841
self.stdout.write("")
@@ -782,8 +847,16 @@ def print_summary(self, elapsed_seconds):
782847
self.stdout.write(f" Tags: {self.stats['tags']}")
783848
self.stdout.write(f" Contacts: {self.stats['contacts']}")
784849
self.stdout.write(f" Accounts: {self.stats['accounts']}")
850+
self.stdout.write(f" Products: {self.stats['products']}")
851+
self.stdout.write(f" Invoice Templates: {self.stats['invoice_templates']}")
785852
self.stdout.write(f" Leads: {self.stats['leads']}")
786853
self.stdout.write(f" Opportunities: {self.stats['opportunities']}")
854+
self.stdout.write(f" Invoices: {self.stats['invoices']}")
855+
self.stdout.write(f" Invoice Line Items: {self.stats['invoice_line_items']}")
856+
self.stdout.write(f" Payments: {self.stats['payments']}")
857+
self.stdout.write(f" Estimates: {self.stats['estimates']}")
858+
self.stdout.write(f" Estimate Line Items: {self.stats['estimate_line_items']}")
859+
self.stdout.write(f" Recurring Invoices: {self.stats['recurring_invoices']}")
787860
self.stdout.write(f" Cases: {self.stats['cases']}")
788861
self.stdout.write(f" Tasks: {self.stats['tasks']}")
789862
self.stdout.write("")

0 commit comments

Comments
 (0)