Skip to content

Commit 50ca6a3

Browse files
committed
Fix webhooks
1 parent d069e3f commit 50ca6a3

File tree

4 files changed

+200
-3
lines changed

4 files changed

+200
-3
lines changed

landolfio/inventory/management/commands/update_moneybird_ids.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import json
2+
import re
3+
import time
4+
from datetime import datetime
25
from django.core.management.base import BaseCommand, CommandError
36
from inventory.models import Asset
47
from inventory.moneybird import MoneybirdAssetService
@@ -14,6 +17,33 @@ def add_arguments(self, parser):
1417
help='Path to JSON file with asset moneybird IDs'
1518
)
1619

20+
def _wait_for_rate_limit(self, error):
21+
match = re.search(r"Retry after '([^']+)'", str(error))
22+
if match:
23+
retry_after = datetime.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
24+
wait_seconds = max(0, (retry_after - datetime.now()).total_seconds()) + 1
25+
self.stdout.write(self.style.WARNING(f'Rate limited, waiting until {retry_after}...'))
26+
time.sleep(wait_seconds)
27+
return True
28+
return False
29+
30+
def _sync_with_retry(self, asset):
31+
mb = MoneybirdAssetService()
32+
while True:
33+
try:
34+
mb.update_asset(asset_id=asset.moneybird_asset_id, name=str(asset))
35+
break
36+
except Exception as e:
37+
if not self._wait_for_rate_limit(e):
38+
raise
39+
while True:
40+
try:
41+
asset.refresh_from_moneybird()
42+
break
43+
except Exception as e:
44+
if not self._wait_for_rate_limit(e):
45+
raise
46+
1747
def handle(self, *args, **options):
1848
json_file = options['json_file']
1949

@@ -45,9 +75,7 @@ def handle(self, *args, **options):
4575
asset.save(update_fields=['moneybird_asset_id'])
4676

4777
try:
48-
mb = MoneybirdAssetService()
49-
mb.update_asset(asset_id=asset.moneybird_asset_id, name=str(asset))
50-
asset.refresh_from_moneybird()
78+
self._sync_with_retry(asset)
5179
self.stdout.write(
5280
self.style.SUCCESS(f'Updated {asset_name}: {moneybird_id}, synced name to Moneybird, and refreshed data')
5381
)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import logging
2+
3+
from moneybird.resource_types import MoneybirdResourceType
4+
from moneybird.webhooks.events import WebhookEvent
5+
from inventory.models.asset import Asset
6+
from inventory.moneybird import MoneybirdAssetService
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class AssetResourceType(MoneybirdResourceType):
12+
entity_type = "company_assets_asset"
13+
entity_type_name = "asset"
14+
api_path = "assets"
15+
public_path = "assets"
16+
model = Asset
17+
can_write = False
18+
can_delete = False
19+
can_do_full_sync = False
20+
21+
@classmethod
22+
def get_model_kwargs(cls, data):
23+
return {"moneybird_asset_id": str(data["id"])}
24+
25+
@classmethod
26+
def get_queryset(cls):
27+
return cls.model.objects.all()
28+
29+
@classmethod
30+
def get_moneybird_ids(cls):
31+
return list(
32+
filter(
33+
None,
34+
cls.get_queryset().values_list("moneybird_asset_id", flat=True),
35+
)
36+
)
37+
38+
@classmethod
39+
def process_webhook_event(
40+
cls,
41+
resource_id: str,
42+
data: dict,
43+
event: WebhookEvent,
44+
):
45+
logger.info(f"Processing {event} for {cls.entity_type_name} {resource_id}")
46+
47+
if not data:
48+
return cls.delete_from_moneybird(resource_id)
49+
50+
try:
51+
asset = cls.get_queryset().get(moneybird_asset_id=resource_id)
52+
logger.info(f"Asset {resource_id} already exists locally, refreshing")
53+
asset.refresh_from_moneybird()
54+
asset.update_on_moneybird()
55+
return asset
56+
except cls.model.DoesNotExist:
57+
asset_name = data.get("name", "")
58+
if asset_name:
59+
try:
60+
asset = cls.get_queryset().get(
61+
name=asset_name, moneybird_asset_id__isnull=True
62+
)
63+
logger.info(
64+
f"Found existing asset with name '{asset_name}', linking to Moneybird asset {resource_id}"
65+
)
66+
asset.moneybird_asset_id = resource_id
67+
asset._refresh_from_moneybird(data)
68+
return asset
69+
except cls.model.DoesNotExist:
70+
logger.info(
71+
f"No existing asset found with name '{asset_name}' to link"
72+
)
73+
except cls.model.MultipleObjectsReturned:
74+
logger.warning(
75+
f"Multiple assets found with name '{asset_name}', cannot auto-link"
76+
)
77+
else:
78+
logger.warning(f"Asset {resource_id} has no name, cannot match")
79+
return None
80+
81+
@classmethod
82+
def delete_from_moneybird(cls, resource_id: str):
83+
logger.info(f"Asset {resource_id} deleted on Moneybird")
84+
try:
85+
asset = cls.get_queryset().get(moneybird_asset_id=resource_id)
86+
logger.info(
87+
f"Unlinking local asset {asset.id} from Moneybird asset {resource_id}"
88+
)
89+
asset.moneybird_asset_id = None
90+
asset.moneybird_data = None
91+
asset.disposal = None
92+
asset.current_value = None
93+
asset.save(
94+
update_fields=[
95+
"moneybird_asset_id",
96+
"moneybird_data",
97+
"disposal",
98+
"current_value",
99+
]
100+
)
101+
except cls.model.DoesNotExist:
102+
logger.info(
103+
f"Asset {resource_id} not found locally, nothing to unlink"
104+
)

landolfio/moneybird/webhooks/events.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,66 @@ class WebhookEvent(Enum):
544544
WORKFLOW_DEACTIVATED = "workflow_deactivated" # Workflow deactivated
545545
WORKFLOW_DESTROYED = "workflow_destroyed" # Workflow deleted
546546
WORKFLOW_UPDATED = "workflow_updated" # Workflow updated
547+
548+
COMPANY_ASSETS_ASSET = "company_assets_asset"
549+
550+
COMPANY_ASSETS_ASSET_CREATED = "company_assets_asset_created" # Asset created
551+
COMPANY_ASSETS_ASSET_DESTROYED = "company_assets_asset_destroyed" # Asset destroyed
552+
COMPANY_ASSETS_ASSET_UPDATED = "company_assets_asset_updated" # Asset updated
553+
COMPANY_ASSETS_DISPOSAL_CREATED = (
554+
"company_assets_disposal_created" # Asset disposed
555+
)
556+
COMPANY_ASSETS_DISPOSAL_DESTROYED = (
557+
"company_assets_disposal_destroyed" # Asset disposal reversed
558+
)
559+
COMPANY_ASSETS_SOURCE_CREATED = (
560+
"company_assets_source_created" # Invoice detail linked to asset
561+
)
562+
COMPANY_ASSETS_SOURCE_DESTROYED = (
563+
"company_assets_source_destroyed" # Invoice detail unlinked from asset
564+
)
565+
COMPANY_ASSETS_VALUE_CHANGES_LINEAR_CREATED = (
566+
"company_assets_value_changes_linear_created" # Linear depreciation created
567+
)
568+
COMPANY_ASSETS_VALUE_CHANGES_LINEAR_DESTROYED = (
569+
"company_assets_value_changes_linear_destroyed" # Linear depreciation destroyed
570+
)
571+
COMPANY_ASSETS_VALUE_CHANGES_ARBITRARY_CREATED = (
572+
"company_assets_value_changes_arbitrary_created" # Arbitrary depreciation created
573+
)
574+
COMPANY_ASSETS_VALUE_CHANGES_ARBITRARY_DESTROYED = (
575+
"company_assets_value_changes_arbitrary_destroyed" # Arbitrary depreciation deleted
576+
)
577+
COMPANY_ASSETS_VALUE_CHANGES_DIVESTMENT_CREATED = (
578+
"company_assets_value_changes_divestment_created" # Divestment created
579+
)
580+
COMPANY_ASSETS_VALUE_CHANGES_DIVESTMENT_DESTROYED = (
581+
"company_assets_value_changes_divestment_destroyed" # Divestment deleted
582+
)
583+
COMPANY_ASSETS_VALUE_CHANGES_FULL_DEPRECIATION_CREATED = (
584+
"company_assets_value_changes_full_depreciation_created" # Full depreciation created
585+
)
586+
COMPANY_ASSETS_VALUE_CHANGES_FULL_DEPRECIATION_DESTROYED = (
587+
"company_assets_value_changes_full_depreciation_destroyed" # Full depreciation destroyed
588+
)
589+
COMPANY_ASSETS_VALUE_CHANGES_MANUAL_CREATED = (
590+
"company_assets_value_changes_manual_created" # Manual value change created
591+
)
592+
COMPANY_ASSETS_VALUE_CHANGES_MANUAL_DESTROYED = (
593+
"company_assets_value_changes_manual_destroyed" # Manual value change deleted
594+
)
595+
COMPANY_ASSETS_VALUE_CHANGE_PLAN_CREATED = (
596+
"company_assets_value_change_plan_created" # Depreciation plan created
597+
)
598+
COMPANY_ASSETS_VALUE_CHANGE_PLAN_DESTROYED = (
599+
"company_assets_value_change_plan_destroyed" # Depreciation plan destroyed
600+
)
601+
COMPANY_ASSETS_VALUE_CHANGE_PLAN_UPDATED = (
602+
"company_assets_value_change_plan_updated" # Depreciation plan updated
603+
)
604+
COMPANY_ASSETS_VALUE_CHANGE_PLAN_DEACTIVATED = (
605+
"company_assets_value_change_plan_deactivated" # Depreciation plan deactivated
606+
)
607+
COMPANY_ASSETS_VALUE_CHANGE_PLAN_ACTIVATED = (
608+
"company_assets_value_change_plan_activated" # Depreciation plan activated
609+
)

landolfio/website/settings/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
MONEYBIRD_RESOURCE_TYPES = [
137137
"accounting.models.contact.ContactResourceType",
138138
"accounting.models.subscription.SubscriptionResourceType",
139+
"inventory.resource_types.AssetResourceType",
139140
]
140141
MONEYBIRD_WEBHOOK_EVENTS = [
141142
WebhookEvent.CONTACT,
@@ -144,6 +145,7 @@
144145
WebhookEvent.SUBSCRIPTION_UPDATED,
145146
WebhookEvent.SUBSCRIPTION_EDITED,
146147
WebhookEvent.SUBSCRIPTION_DESTROYED,
148+
WebhookEvent.COMPANY_ASSETS_ASSET,
147149
]
148150
MONEYBIRD_WEBHOOK_ID = os.environ.get("MONEYBIRD_WEBHOOK_ID")
149151
MONEYBIRD_WEBHOOK_TOKEN = os.environ.get("MONEYBIRD_WEBHOOK_TOKEN")

0 commit comments

Comments
 (0)