Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merchandise sales moneybird integration #3464

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
23246b3
Implement merchandise sale in backend and to mb
tmgdejong Oct 16, 2023
f74f5cb
parent cb8b4d9d631465e54739de5f58d58fa85eedf785
tmgdejong Oct 17, 2023
6ee7177
Correct ledger and small fixes
tmgdejong Oct 18, 2023
23f8672
Textual changes
tmgdejong Oct 19, 2023
c708f32
Remove TPay and Wire as options
tmgdejong Oct 18, 2023
26e69ca
Switch merchandise sales to sales
tmgdejong Oct 26, 2023
c4aa4c5
Continuing merch sales
tmgdejong Oct 26, 2023
393cbeb
Continuing merch sales
tmgdejong Oct 26, 2023
3859882
Small changes
tmgdejong Oct 30, 2023
9dc3e03
Merge branch 'master' into merchandise-sales-
tmgdejong Oct 30, 2023
97d2aa2
Fix migrations
tmgdejong Oct 30, 2023
f3e58f3
Removed comment
tmgdejong Oct 30, 2023
2cca5d7
Merge branch 'merchandise-sales-moneybird-integration' of github.com:…
tmgdejong Oct 30, 2023
c76f085
Some small changes wrt classes and names
tmgdejong Oct 31, 2023
9a8b60b
Small fix
tmgdejong Oct 31, 2023
4e32bea
Merge branch 'master' into merchandise-sales-moneybird-integration
tmgdejong Oct 31, 2023
4df93b9
Added attribute id
tmgdejong Nov 1, 2023
28f853f
correct api call for attr id
tmgdejong Nov 1, 2023
a95af92
Fixed migration
tmgdejong Nov 1, 2023
8f3fee0
Small fixes
tmgdejong Nov 1, 2023
0868f73
Add merchandise item syncing to Moneybird
tmgdejong Nov 6, 2023
ccb02cd
Merge branch 'master' into merchandise-sales-moneybird-integration
tmgdejong Nov 6, 2023
8a8b28f
Remove unused import
tmgdejong Nov 6, 2023
f75feb3
Rewrite to FKs
JobDoesburg Nov 7, 2023
540dc55
Move changes to merchandise
DeD1rk Nov 7, 2023
73fe1ce
Fix admin
JobDoesburg Nov 12, 2023
864f33a
Fix admin
JobDoesburg Nov 12, 2023
2ce0433
Fix moneybird
JobDoesburg Nov 12, 2023
67509f0
Fix bugs
JobDoesburg Nov 12, 2023
7b337ed
Sync individual items
JobDoesburg Nov 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions website/merchandise/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Registers admin interfaces for the models defined in this module."""

from django.contrib import admin
from django.contrib.admin import ModelAdmin

Expand All @@ -13,6 +14,10 @@ class MerchandiseItemAdmin(ModelAdmin):
fields = (
"name",
"price",
"purchase_price",
"description",
"image",
)
search_fields = ("name", "description")
list_display = ("name", "price", "purchase_price")
list_filter = ("name", "price", "purchase_price")
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Generated by Django 4.2.6 on 2023-10-26 14:52

from django.db import migrations, models
import django.db.models.deletion


itemlist = []


class Migration(migrations.Migration):
dependencies = [
("sales", "0007_alter_productlistitem_options_and_more"),
("merchandise", "0011_alter_merchandiseitem_image"),
]

def store_and_delete_merchandiseitems(apps, schema_editor):
MerchandiseItem = apps.get_model("merchandise", "MerchandiseItem")
itemlist = list(MerchandiseItem.objects.all())
MerchandiseItem.objects.all().delete()

# This does not seem to work yet
tmgdejong marked this conversation as resolved.
Show resolved Hide resolved
def create_merchandiseitems(apps, schema_editor):
MerchandiseItem = apps.get_model("merchandise", "MerchandiseItem")
for item in itemlist:
MerchandiseItem.objects.create(
name=item.name,
price=item.price,
description=item.description,
image=item.image,
purchase_price=0,
)

operations = [
migrations.RunPython(store_and_delete_merchandiseitems),
migrations.RemoveField(
model_name="merchandiseitem",
name="id",
),
migrations.RemoveField(
model_name="merchandiseitem",
name="name",
),
migrations.AddField(
model_name="merchandiseitem",
name="product_ptr",
field=models.OneToOneField(
auto_created=True,
default="",
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="sales.product",
),
preserve_default=False,
),
migrations.AddField(
model_name="merchandiseitem",
name="purchase_price",
field=models.DecimalField(decimal_places=2, max_digits=8),
),
migrations.RunPython(create_merchandiseitems),
]
12 changes: 8 additions & 4 deletions website/merchandise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,31 @@

from thumbnails.fields import ImageField

from sales.models.product import Product
from thaliawebsite.storage.backend import get_public_storage
from utils.media.services import get_upload_to_function

_merchandise_photo_upload_to = get_upload_to_function("merchandise")


class MerchandiseItem(models.Model):
class MerchandiseItem(Product):
"""Merchandise items.

This model describes merchandise items.
"""

#: Name of the merchandise item.
name = models.CharField(max_length=200)

#: Price of the merchandise item
price = models.DecimalField(
max_digits=8,
decimal_places=2,
)

#: Purchase price of the merchandise item
purchase_price = models.DecimalField(
tmgdejong marked this conversation as resolved.
Show resolved Hide resolved
max_digits=8,
decimal_places=2,
)

#: Description of the merchandise item
description = models.TextField()

Expand Down
35 changes: 34 additions & 1 deletion website/moneybirdsynchronization/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django.contrib import admin
from django.contrib.admin import RelatedOnlyFieldListFilter

from .models import MoneybirdContact, MoneybirdExternalInvoice, MoneybirdPayment
from .models import (
MoneybirdContact,
MoneybirdExternalInvoice,
MoneybirdGeneralJournalDocument,
MoneybirdPayment,
)


@admin.register(MoneybirdContact)
Expand Down Expand Up @@ -100,3 +105,31 @@ def get_readonly_fields(self, request, obj=None):
if not obj:
return ()
return ("payment",)


@admin.register(MoneybirdGeneralJournalDocument)
class MoneybirdGeneralJournalDocumentAdmin(admin.ModelAdmin):
list_display = (
"order",
"moneybird_general_journal_document_id",
"needs_synchronization",
"needs_deletion",
)

readonly_fields = (
"order",
"moneybird_general_journal_document_id",
"needs_synchronization",
"needs_deletion",
)

fields = (
"order",
"moneybird_general_journal_document_id",
"external_invoice",
)

list_filter = (
"needs_synchronization",
"needs_deletion",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Generated by Django 4.2.6 on 2023-10-26 20:44

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("sales", "0007_alter_productlistitem_options_and_more"),
(
"moneybirdsynchronization",
"0007_alter_moneybirdpayment_moneybird_financial_mutation_id_and_more",
),
]

operations = [
migrations.CreateModel(
name="MoneybirdGeneralJournalDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"moneybird_general_journal_document_id",
models.CharField(
blank=True,
max_length=255,
null=True,
verbose_name="moneybird general journal document id",
),
),
(
"external_invoice",
models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="moneybird_journal_external_invoice",
to="moneybirdsynchronization.moneybirdexternalinvoice",
verbose_name="external invoice",
),
),
(
"order",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to="sales.order",
verbose_name="order",
),
),
(
"needs_deletion",
models.BooleanField(
default=False,
help_text="Indicates that the journal has to be deleted from moneybird.",
),
),
(
"needs_synchronization",
models.BooleanField(
default=True,
help_text="Indicates that the journal has to be synchronized (again).",
),
),
],
options={
"verbose_name": "moneybird general journal document",
"verbose_name_plural": "moneybird general journal documents",
},
),
]
93 changes: 89 additions & 4 deletions website/moneybirdsynchronization/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from events.models import EventRegistration
from members.models import Member, Membership
from merchandise.models import MerchandiseItem
from moneybirdsynchronization.moneybird import get_moneybird_api_service
from payments.models import BankAccount, Payment
from payments.payables import payables
Expand Down Expand Up @@ -41,6 +42,8 @@ def project_name_for_payable_model(obj) -> Optional[str]:
return f"{obj.shift} [{start_date}]"
if isinstance(obj, (Registration, Renewal)):
return None
if isinstance(obj, Order):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unreachable

return None

raise ValueError(f"Unknown payable model {obj}")

Expand All @@ -54,14 +57,19 @@ def date_for_payable_model(obj) -> Union[datetime.datetime, datetime.date]:
return obj.shift.start
if isinstance(obj, (Registration, Renewal)):
return obj.created_at.date()
if isinstance(obj, Order):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unreachable: there's already a branch for Order in line 55

return obj.created_at.date()

raise ValueError(f"Unknown payable model {obj}")


def ledger_id_for_payable_model(obj) -> Optional[int]:
if isinstance(obj, (Registration, Renewal)):
return settings.MONEYBIRD_CONTRIBUTION_LEDGER_ID
return None
if isinstance(obj, Order):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be incorrect for normal sales orders

return settings.MONEYBIRD_MERCHANDISE_SALES_LEDGER_ID

raise ValueError(f"Unknown payable model {obj}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should just return none for the payables right?



def membership_to_mb_period(membership: Membership) -> str:
Expand Down Expand Up @@ -311,9 +319,9 @@ def to_moneybird(self):
project_id
)
if ledger_id is not None:
data["external_sales_invoice"]["details_attributes"][0]["ledger_id"] = int(
ledger_id
)
data["external_sales_invoice"]["details_attributes"][0][
"ledger_account_id"
] = int(ledger_id)

if self.moneybird_details_attribute_id is not None:
data["external_sales_invoice"]["details_attributes"][0]["id"] = int(
Expand Down Expand Up @@ -389,3 +397,80 @@ def to_moneybird(self):
class Meta:
verbose_name = _("moneybird payment")
verbose_name_plural = _("moneybird payments")


class MoneybirdGeneralJournalDocument(models.Model):
order = models.OneToOneField(
Order,
on_delete=models.CASCADE,
verbose_name=_("order"),
)

moneybird_general_journal_document_id = models.CharField(
verbose_name=_("moneybird general journal document id"),
max_length=255,
blank=True,
null=True,
)

external_invoice = models.OneToOneField(
MoneybirdExternalInvoice,
on_delete=models.DO_NOTHING,
verbose_name=_("external invoice"),
related_name="moneybird_journal_external_invoice",
blank=True,
null=True,
)

needs_synchronization = models.BooleanField(
default=True, # The field is set False only when it has been successfully synchronized.
help_text="Indicates that the journal has to be synchronized (again).",
)

needs_deletion = models.BooleanField(
default=False,
help_text="Indicates that the journal has to be deleted from moneybird.",
)

def __str__(self):
return f"Moneybird journal for {self.order}"

def to_moneybird(self):
tmgdejong marked this conversation as resolved.
Show resolved Hide resolved
items = self.order.order_items.all()
total_purchase_amount = sum(
MerchandiseItem.objects.get(name=i.product.product.name).purchase_price
* i.amount
for i in items
)

merchandise_stock_ledger_id = settings.MONEYBIRD_MERCHANDISE_STOCK_LEDGER_ID
merchandise_costs_ledger_id = settings.MONEYBIRD_MERCHANDISE_COSTS_LEDGER_ID

data = {
"general_journal_document": {
"date": self.order.payment.created_at.strftime("%Y-%m-%d"),
"reference": f"M {self.external_invoice.payable.model.pk}",
"general_journal_document_entries_attributes": {
"0": {
"ledger_account_id": merchandise_costs_ledger_id,
"debit": str(total_purchase_amount),
"credit": "0",
"description": self.order.payment.notes,
"contact_id": self.external_invoice.payable.payment_payer.moneybird_contact.moneybird_id,
},
"1": {
"ledger_account_id": merchandise_stock_ledger_id,
"debit": "0",
"credit": str(total_purchase_amount),
"description": self.order.payment.notes,
"contact_id": self.external_invoice.payable.payment_payer.moneybird_contact.moneybird_id,
},
},
}
}

return data

class Meta:
verbose_name = _("moneybird general journal document")
verbose_name_plural = _("moneybird general journal documents")
15 changes: 15 additions & 0 deletions website/moneybirdsynchronization/moneybird.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ def unlink_mutation_from_booking(
{"booking_type": booking_type, "booking_id": booking_id},
)

def create_general_journal_document(self, document_data):
return self._administration.post(
"documents/general_journal_documents", document_data
)

def update_general_journal_document(self, document_id, document_data):
return self._administration.patch(
f"documents/general_journal_documents/{document_id}", document_data
)

def delete_general_journal_document(self, document_id):
return self._administration.delete(
f"documents/general_journal_documents/{document_id}"
)


def get_moneybird_api_service():
if (
Expand Down
Loading
Loading