Compare commits
9 commits
4b3ddec4bb
...
cdd8838b3d
Author | SHA1 | Date | |
---|---|---|---|
cdd8838b3d | |||
e18cafa813 | |||
22ff769b2c | |||
8f75db5325 | |||
6ce13126d5 | |||
0bd620d68e | |||
549e1fa19a | |||
97fc045375 | |||
1ed383ea10 |
24 changed files with 171 additions and 545 deletions
|
@ -4,13 +4,6 @@ on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
paths:
|
|
||||||
- "deployment/**"
|
|
||||||
- "docker/**"
|
|
||||||
- "src/**"
|
|
||||||
- "Dockerfile"
|
|
||||||
- "pyproject.toml"
|
|
||||||
- "uv.lock"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -51,7 +44,7 @@ jobs:
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
@ -87,7 +80,7 @@ jobs:
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Deploy to OpenShift
|
- name: Deploy to OpenShift
|
||||||
uses: docker://quay.io/appuio/oc:v4.18
|
uses: docker://quay.io/appuio/oc:v4.16
|
||||||
with:
|
with:
|
||||||
entrypoint: /bin/bash
|
entrypoint: /bin/bash
|
||||||
args: |
|
args: |
|
||||||
|
@ -104,7 +97,7 @@ jobs:
|
||||||
OPENSHIFT_URL: ${{ secrets.OPENSHIFT_URL }}
|
OPENSHIFT_URL: ${{ secrets.OPENSHIFT_URL }}
|
||||||
|
|
||||||
- name: Verify deployment
|
- name: Verify deployment
|
||||||
uses: docker://quay.io/appuio/oc:v4.18
|
uses: docker://quay.io/appuio/oc:v4.16
|
||||||
with:
|
with:
|
||||||
entrypoint: /bin/bash
|
entrypoint: /bin/bash
|
||||||
args: |
|
args: |
|
||||||
|
|
|
@ -3,13 +3,6 @@ name: Build and Deploy Staging
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
|
||||||
- "deployment/**"
|
|
||||||
- "docker/**"
|
|
||||||
- "src/**"
|
|
||||||
- "Dockerfile"
|
|
||||||
- "pyproject.toml"
|
|
||||||
- "uv.lock"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -35,7 +28,7 @@ jobs:
|
||||||
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
|
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
@ -56,7 +49,7 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Deploy to OpenShift
|
- name: Deploy to OpenShift
|
||||||
uses: docker://quay.io/appuio/oc:v4.18
|
uses: docker://quay.io/appuio/oc:v4.16
|
||||||
with:
|
with:
|
||||||
entrypoint: /bin/bash
|
entrypoint: /bin/bash
|
||||||
args: |
|
args: |
|
||||||
|
|
|
@ -30,7 +30,7 @@ jobs:
|
||||||
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
|
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docs/Dockerfile
|
file: docs/Dockerfile
|
||||||
|
@ -52,7 +52,7 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Deploy to OpenShift
|
- name: Deploy to OpenShift
|
||||||
uses: docker://quay.io/appuio/oc:v4.18
|
uses: docker://quay.io/appuio/oc:v4.16
|
||||||
with:
|
with:
|
||||||
entrypoint: /bin/bash
|
entrypoint: /bin/bash
|
||||||
args: |
|
args: |
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
name: Renovate Dependency Bot
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 3 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
renovate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- name: Renovate
|
|
||||||
uses: https://github.com/renovatebot/github-action@v42.0.4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
|
||||||
env:
|
|
||||||
LOG_LEVEL: info
|
|
||||||
RENOVATE_ENDPOINT: ${{ vars.RENOVATE_ENDPOINT }}
|
|
||||||
RENOVATE_PLATFORM: gitea
|
|
||||||
RENOVATE_REPOSITORIES: ${{ github.repository }}
|
|
||||||
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_TOKEN }}
|
|
||||||
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate@servala.app.codey.ch>"
|
|
||||||
RENOVATE_USERNAME: renovate
|
|
||||||
RENOVATE_ENABLE_PYTHON_TOOL_VERSIONS: true
|
|
|
@ -2,10 +2,6 @@ name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
|
||||||
- "src/**"
|
|
||||||
- "pyproject.toml"
|
|
||||||
- "uv.lock"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -21,7 +17,7 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: https://github.com/astral-sh/setup-uv@v6
|
uses: https://github.com/astral-sh/setup-uv@v5
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: uv run --env-file=.env.example pytest
|
run: uv run --env-file=.env.example pytest
|
||||||
|
|
|
@ -2,4 +2,3 @@ resources:
|
||||||
- deployment.yaml
|
- deployment.yaml
|
||||||
- service.yaml
|
- service.yaml
|
||||||
- cronjob.yaml
|
- cronjob.yaml
|
||||||
- objectstorage.yaml
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
apiVersion: appcat.vshn.io/v1
|
|
||||||
kind: ObjectBucket
|
|
||||||
metadata:
|
|
||||||
name: portal-storage
|
|
||||||
spec:
|
|
||||||
parameters:
|
|
||||||
region: lpg
|
|
||||||
writeConnectionSecretToRef:
|
|
||||||
name: portal-storage-creds
|
|
|
@ -11,4 +11,3 @@ resources:
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
patches:
|
patches:
|
||||||
- path: portal-deployment.yaml
|
- path: portal-deployment.yaml
|
||||||
- path: objectstorage.yaml
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
apiVersion: appcat.vshn.io/v1
|
|
||||||
kind: ObjectBucket
|
|
||||||
metadata:
|
|
||||||
name: portal-storage
|
|
||||||
spec:
|
|
||||||
parameters:
|
|
||||||
bucketName: servala-portal-storage-production
|
|
|
@ -11,4 +11,3 @@ resources:
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
patches:
|
patches:
|
||||||
- path: portal-deployment.yaml
|
- path: portal-deployment.yaml
|
||||||
- path: objectstorage.yaml
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
apiVersion: appcat.vshn.io/v1
|
|
||||||
kind: ObjectBucket
|
|
||||||
metadata:
|
|
||||||
name: portal-storage
|
|
||||||
spec:
|
|
||||||
parameters:
|
|
||||||
bucketName: servala-portal-storage-staging
|
|
|
@ -7,7 +7,7 @@ requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2-cffi>=23.1.0",
|
"argon2-cffi>=23.1.0",
|
||||||
"cryptography>=44.0.2",
|
"cryptography>=44.0.2",
|
||||||
"django==5.2.1",
|
"django==5.2",
|
||||||
"django-allauth>=65.5.0",
|
"django-allauth>=65.5.0",
|
||||||
"django-fernet-encrypted-fields>=0.3.0",
|
"django-fernet-encrypted-fields>=0.3.0",
|
||||||
"django-scopes>=2.0.0",
|
"django-scopes>=2.0.0",
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended"
|
|
||||||
],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchManagers": [
|
|
||||||
"github-actions"
|
|
||||||
],
|
|
||||||
"matchFileNames": [
|
|
||||||
".forgejo/workflows/*.yml",
|
|
||||||
".forgejo/workflows/*.yaml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchManagers": [
|
|
||||||
"pep621"
|
|
||||||
],
|
|
||||||
"rangeStrategy": "bump"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackageNames": [
|
|
||||||
"python"
|
|
||||||
],
|
|
||||||
"matchManagers": [
|
|
||||||
"dockerfile"
|
|
||||||
],
|
|
||||||
"versioning": "docker"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"labels": [
|
|
||||||
"dependencies"
|
|
||||||
],
|
|
||||||
"lockFileMaintenance": {
|
|
||||||
"enabled": true,
|
|
||||||
"schedule": [
|
|
||||||
"before 5am on monday"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"prConcurrentLimit": 5,
|
|
||||||
"branchConcurrentLimit": 10
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import rules
|
import rules
|
||||||
import urlman
|
import urlman
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models, transaction
|
from django.db import models
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -9,7 +9,6 @@ from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
from servala.core import rules as perms
|
from servala.core import rules as perms
|
||||||
from servala.core.models.mixins import ServalaModelMixin
|
from servala.core.models.mixins import ServalaModelMixin
|
||||||
from servala.core.odoo import CLIENT
|
|
||||||
|
|
||||||
|
|
||||||
class Organization(ServalaModelMixin, models.Model):
|
class Organization(ServalaModelMixin, models.Model):
|
||||||
|
@ -126,63 +125,9 @@ class BillingEntity(ServalaModelMixin, models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@transaction.atomic
|
def create_from_data(cls, odoo_data):
|
||||||
def create_from_data(cls, name, odoo_data):
|
instance = BillingEntity.objects.create(name=odoo_data.get("name"))
|
||||||
"""
|
# TODO implement odoo creation from data
|
||||||
Creates a BillingEntity and corresponding Odoo records.
|
|
||||||
|
|
||||||
This method creates a `res.partner` record in Odoo with `company_type='company'`
|
|
||||||
for the main company, and another `res.partner` record with `company_type='person'`
|
|
||||||
and `type='invoice'` (linked via `parent_id` to the first record) for the
|
|
||||||
invoice address. The IDs of these Odoo records are stored in the BillingEntity.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
odoo_data (dict): A dictionary containing the data for creating
|
|
||||||
the BillingEntity and Odoo records.
|
|
||||||
|
|
||||||
Expected keys in `odoo_data`:
|
|
||||||
- `invoice_street` (str): Street for the invoice address.
|
|
||||||
- `invoice_street2` (str): Second line of street address for the invoice address.
|
|
||||||
- `invoice_city` (str): City for the invoice address.
|
|
||||||
- `invoice_zip` (str): ZIP/Postal code for the invoice address.
|
|
||||||
- `invoice_country` (int): Odoo `res.country` ID for the invoice address country.
|
|
||||||
- `invoice_email` (str): Email address for the invoice contact.
|
|
||||||
- `invoice_phone` (str): Phone number for the invoice contact.
|
|
||||||
"""
|
|
||||||
instance = cls.objects.create(name=name)
|
|
||||||
company_payload = {
|
|
||||||
"name": odoo_data.get("company_name", name),
|
|
||||||
"company_type": "company",
|
|
||||||
}
|
|
||||||
if vat := odoo_data.get("invoice_vat"):
|
|
||||||
company_payload["vat"] = vat
|
|
||||||
company_id = CLIENT.execute("res.partner", "create", [company_payload])
|
|
||||||
instance.odoo_company_id = company_id
|
|
||||||
|
|
||||||
invoice_address_payload = {
|
|
||||||
"name": name,
|
|
||||||
"company_type": "person",
|
|
||||||
"type": "invoice",
|
|
||||||
"parent_id": company_id,
|
|
||||||
}
|
|
||||||
invoice_optional_fields = {
|
|
||||||
"street": odoo_data.get("invoice_street"),
|
|
||||||
"street2": odoo_data.get("invoice_street2"),
|
|
||||||
"city": odoo_data.get("invoice_city"),
|
|
||||||
"zip": odoo_data.get("invoice_zip"),
|
|
||||||
"country_id": odoo_data.get("invoice_country"),
|
|
||||||
"email": odoo_data.get("invoice_email"),
|
|
||||||
}
|
|
||||||
invoice_address_payload.update(
|
|
||||||
{k: v for k, v in invoice_optional_fields.items() if v is not None}
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice_address_id = CLIENT.execute(
|
|
||||||
"res.partner", "create", [invoice_address_payload]
|
|
||||||
)
|
|
||||||
instance.odoo_invoice_id = invoice_address_id
|
|
||||||
|
|
||||||
instance.save(update_fields=["odoo_company_id", "odoo_invoice_id"])
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -192,49 +137,6 @@ class BillingEntity(ServalaModelMixin, models.Model):
|
||||||
# return instance
|
# return instance
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def odoo_data(self):
|
|
||||||
data = {
|
|
||||||
"company": None,
|
|
||||||
"invoice_address": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
company_fields = ["name", "company_type", "vat"]
|
|
||||||
invoice_address_fields = [
|
|
||||||
"name",
|
|
||||||
"company_type",
|
|
||||||
"type",
|
|
||||||
"parent_id",
|
|
||||||
"street",
|
|
||||||
"street2",
|
|
||||||
"city",
|
|
||||||
"zip",
|
|
||||||
"country_id",
|
|
||||||
"email",
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.odoo_company_id:
|
|
||||||
company_records = CLIENT.search_read(
|
|
||||||
model="res.partner",
|
|
||||||
domain=[["id", "=", self.odoo_company_id]],
|
|
||||||
fields=company_fields,
|
|
||||||
limit=1,
|
|
||||||
)
|
|
||||||
if company_records:
|
|
||||||
data["company"] = company_records[0]
|
|
||||||
|
|
||||||
if self.odoo_invoice_id:
|
|
||||||
invoice_address_records = CLIENT.search_read(
|
|
||||||
model="res.partner",
|
|
||||||
domain=[["id", "=", self.odoo_invoice_id]],
|
|
||||||
fields=invoice_address_fields,
|
|
||||||
limit=1,
|
|
||||||
)
|
|
||||||
if invoice_address_records:
|
|
||||||
data["invoice_address"] = invoice_address_records[0]
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationOrigin(ServalaModelMixin, models.Model):
|
class OrganizationOrigin(ServalaModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -87,33 +87,6 @@ class OdooClient:
|
||||||
|
|
||||||
CLIENT = OdooClient()
|
CLIENT = OdooClient()
|
||||||
|
|
||||||
# Odoo countries do not change, so they are fetched once per process
|
|
||||||
COUNTRIES = []
|
|
||||||
|
|
||||||
|
|
||||||
def get_odoo_countries():
|
|
||||||
global COUNTRIES
|
|
||||||
if COUNTRIES:
|
|
||||||
return COUNTRIES
|
|
||||||
|
|
||||||
try:
|
|
||||||
odoo_countries_data = CLIENT.search_read(
|
|
||||||
model="res.country", domain=[], fields=["id", "name"]
|
|
||||||
)
|
|
||||||
# Format as Django choices: [(value, label), ...]
|
|
||||||
COUNTRIES = [
|
|
||||||
(country["id"], country["name"]) for country in odoo_countries_data
|
|
||||||
]
|
|
||||||
# Sort by country name for better UX in dropdowns
|
|
||||||
COUNTRIES.sort(key=lambda x: x[1])
|
|
||||||
except Exception as e:
|
|
||||||
# Log the error or handle it as appropriate for your application
|
|
||||||
# For now, return an empty list or a default if Odoo is unavailable
|
|
||||||
print(f"Error fetching Odoo countries: {e}")
|
|
||||||
return [("", "Error fetching countries")] # Or just []
|
|
||||||
|
|
||||||
return COUNTRIES
|
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_addresses(user):
|
def get_invoice_addresses(user):
|
||||||
"""Used during organization creation: retrieves all invoice
|
"""Used during organization creation: retrieves all invoice
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.forms import ModelForm
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from servala.core.models import Organization
|
from servala.core.models import Organization
|
||||||
from servala.core.odoo import get_invoice_addresses, get_odoo_countries
|
from servala.core.odoo import get_invoice_addresses
|
||||||
from servala.frontend.forms.mixins import HtmxMixin
|
from servala.frontend.forms.mixins import HtmxMixin
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,34 +28,33 @@ class OrganizationCreateForm(OrganizationForm):
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fields for creating a new billing address in Odoo, prefixed with 'invoice_'
|
# Fields for creating a new billing address in Odoo, prefixed with 'ba_'
|
||||||
invoice_street = forms.CharField(label=_("Line 1"), required=False, max_length=100)
|
ba_name = forms.CharField(
|
||||||
invoice_street2 = forms.CharField(label=_("Line 2"), required=False, max_length=100)
|
label=_("Contact Person / Company Name"), required=False, max_length=100
|
||||||
invoice_city = forms.CharField(label=_("City"), required=False, max_length=100)
|
|
||||||
invoice_zip = forms.CharField(label=_("Postal Code"), required=False, max_length=20)
|
|
||||||
invoice_country = forms.ChoiceField(
|
|
||||||
label=_("Country"),
|
|
||||||
required=False,
|
|
||||||
choices=get_odoo_countries(),
|
|
||||||
)
|
)
|
||||||
invoice_email = forms.EmailField(label=_("Billing Email"), required=False)
|
ba_street = forms.CharField(label=_("Street"), required=False, max_length=100)
|
||||||
invoice_phone = forms.CharField(label=_("Phone"), required=False, max_length=30)
|
ba_street2 = forms.CharField(
|
||||||
invoice_vat = forms.CharField(label=_("VAT ID"), required=False, max_length=50)
|
label=_("Street 2 (Optional)"), required=False, max_length=100
|
||||||
|
)
|
||||||
|
ba_city = forms.CharField(label=_("City"), required=False, max_length=100)
|
||||||
|
ba_zip = forms.CharField(label=_("ZIP Code"), required=False, max_length=20)
|
||||||
|
# For state & country, Odoo uses structured data. For now, text input.
|
||||||
|
# These will need mapping logic when actual Odoo creation is implemented.
|
||||||
|
ba_state_name = forms.CharField(
|
||||||
|
label=_("State / Province"), required=False, max_length=100
|
||||||
|
)
|
||||||
|
ba_country_name = forms.CharField(
|
||||||
|
label=_("Country"), required=False, max_length=100
|
||||||
|
)
|
||||||
|
ba_email = forms.EmailField(label=_("Billing Email"), required=False)
|
||||||
|
ba_phone = forms.CharField(label=_("Billing Phone"), required=False, max_length=30)
|
||||||
|
ba_vat = forms.CharField(label=_("VAT ID"), required=False, max_length=50)
|
||||||
|
|
||||||
class Meta(OrganizationForm.Meta):
|
class Meta(OrganizationForm.Meta):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self, *args, user=None, **kwargs):
|
def __init__(self, *args, user=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if not self.initial.get("invoice_country"):
|
|
||||||
default_country_name = "Switzerland"
|
|
||||||
country_choices = self.fields["invoice_country"].choices
|
|
||||||
for country_id, country_name_label in country_choices:
|
|
||||||
if country_name_label == default_country_name:
|
|
||||||
self.initial["invoice_country"] = country_id
|
|
||||||
break
|
|
||||||
|
|
||||||
self.user = user
|
self.user = user
|
||||||
self.odoo_addresses = get_invoice_addresses(self.user)
|
self.odoo_addresses = get_invoice_addresses(self.user)
|
||||||
|
|
||||||
|
@ -75,27 +74,39 @@ class OrganizationCreateForm(OrganizationForm):
|
||||||
if not self.is_bound and "billing_processing_choice" not in self.initial:
|
if not self.is_bound and "billing_processing_choice" not in self.initial:
|
||||||
self.fields["billing_processing_choice"].initial = "existing"
|
self.fields["billing_processing_choice"].initial = "existing"
|
||||||
else:
|
else:
|
||||||
self.fields.pop("billing_processing_choice")
|
# No existing Odoo addresses. Force 'new' choice.
|
||||||
|
self.fields["billing_processing_choice"].choices = [
|
||||||
|
("new", _("Create a new billing address")),
|
||||||
|
]
|
||||||
|
self.fields["billing_processing_choice"].initial = "new"
|
||||||
|
self.fields["billing_processing_choice"].widget = forms.HiddenInput()
|
||||||
self.fields["existing_odoo_address_id"].widget = forms.HiddenInput()
|
self.fields["existing_odoo_address_id"].widget = forms.HiddenInput()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
choice = cleaned_data.get("billing_processing_choice")
|
choice = cleaned_data.get("billing_processing_choice")
|
||||||
if not choice or choice == "new":
|
if choice == "new":
|
||||||
required_fields = [
|
required_fields = [
|
||||||
"invoice_street",
|
"ba_name",
|
||||||
"invoice_city",
|
"ba_street",
|
||||||
"invoice_zip",
|
"ba_city",
|
||||||
"invoice_country",
|
"ba_zip",
|
||||||
"invoice_email",
|
"ba_state_name",
|
||||||
|
"ba_country_name",
|
||||||
|
"ba_email",
|
||||||
]
|
]
|
||||||
for field_name in required_fields:
|
for field_name in required_fields:
|
||||||
if not cleaned_data.get(field_name):
|
if not cleaned_data.get(field_name):
|
||||||
self.add_error(field_name, _("This field is required."))
|
self.add_error(
|
||||||
|
field_name,
|
||||||
|
_(
|
||||||
|
"This field is required when creating a new billing address."
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
existing_id_str = cleaned_data.get("existing_odoo_address_id")
|
existing_id_str = cleaned_data.get("existing_odoo_address_id")
|
||||||
if not existing_id_str:
|
if not existing_id_str:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
"existing_odoo_address_id", _("Please select an invoice address.")
|
"existing_odoo_address_id", _("Please select an existing address.")
|
||||||
)
|
)
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from servala.core.models import (
|
from servala.core.models import (
|
||||||
|
@ -19,17 +18,13 @@ class ServiceFilterForm(forms.Form):
|
||||||
cloud_provider = forms.ModelChoiceField(
|
cloud_provider = forms.ModelChoiceField(
|
||||||
queryset=CloudProvider.objects.all(), required=False
|
queryset=CloudProvider.objects.all(), required=False
|
||||||
)
|
)
|
||||||
q = forms.CharField(label=_("Search"), required=False)
|
q = forms.CharField(required=False)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
if category := self.cleaned_data.get("category"):
|
if category := self.cleaned_data.get("category"):
|
||||||
queryset = queryset.filter(category=category)
|
queryset = queryset.filter(category=category)
|
||||||
if cloud_provider := self.cleaned_data.get("cloud_provider"):
|
if cloud_provider := self.cleaned_data.get("cloud_provider"):
|
||||||
queryset = queryset.filter(offerings__provider=cloud_provider)
|
queryset = queryset.filter(offerings__provider=cloud_provider)
|
||||||
if search := self.cleaned_data.get("q"):
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(name__icontains=search) | Q(category__name__icontains=search)
|
|
||||||
)
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,75 +5,31 @@
|
||||||
{% translate "Create a new organization" %}
|
{% translate "Create a new organization" %}
|
||||||
{% endblock page_title %}
|
{% endblock page_title %}
|
||||||
{% endblock html_title %}
|
{% endblock html_title %}
|
||||||
{% block content %}
|
{% block card_content %}
|
||||||
<section class="section">
|
<form method="post" class="form form-vertical">
|
||||||
<form method="post" class="form form-vertical">
|
{% include "frontend/forms/errors.html" %}
|
||||||
<div class="card">
|
{% csrf_token %}
|
||||||
<div class="card-content">
|
<div class="form-body">
|
||||||
<div class="form-body card-body">
|
<div class="row">
|
||||||
<div class="row">
|
{{ form.name.as_field_group }}
|
||||||
{% include "frontend/forms/errors.html" %}
|
<hr class="my-4">
|
||||||
{% csrf_token %}
|
<h4>{% translate "Billing Information" %}</h4>
|
||||||
{{ form.name.as_field_group }}
|
{{ form.billing_processing_choice.as_field_group }}
|
||||||
</div>
|
<div id="existing_billing_address_section" class="mt-3">{{ form.existing_odoo_address_id.as_field_group }}</div>
|
||||||
</div>
|
<div id="new_billing_address_section" class="mt-3">
|
||||||
|
{{ form.ba_name.as_field_group }}
|
||||||
|
{{ form.ba_street.as_field_group }}
|
||||||
|
{{ form.ba_street2.as_field_group }}
|
||||||
|
{{ form.ba_city.as_field_group }}
|
||||||
|
{{ form.ba_zip.as_field_group }}
|
||||||
|
{{ form.ba_state_name.as_field_group }}
|
||||||
|
{{ form.ba_country_name.as_field_group }}
|
||||||
|
{{ form.ba_email.as_field_group }}
|
||||||
|
{{ form.ba_phone.as_field_group }}
|
||||||
|
{{ form.ba_vat.as_field_group }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-sm-12 d-flex justify-content-end">
|
||||||
{% if form.existing_odoo_address_id and form.existing_odoo_address_id.choices %}
|
<button class="btn btn-primary me-1 mb-1" type="submit">{% translate "Create Organization" %}</button>
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">{% translate "Billing Information" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="form-body card-body">
|
|
||||||
<div class="row">
|
|
||||||
{{ form.billing_processing_choice.as_field_group }}
|
|
||||||
<div id="existing_billing_address_section" class="mt-3">{{ form.existing_odoo_address_id.as_field_group }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div id="new_billing_address_section">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">{% translate "Invoice Address" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="form-body card-body">
|
|
||||||
<div class="row">
|
|
||||||
{{ form.invoice_vat.as_field_group }}
|
|
||||||
<hr>
|
|
||||||
{{ form.invoice_street.as_field_group }}
|
|
||||||
{{ form.invoice_street2.as_field_group }}
|
|
||||||
<div class="col-md-2">{{ form.invoice_zip.as_field_group }}</div>
|
|
||||||
<div class="col-md-10">{{ form.invoice_city.as_field_group }}</div>
|
|
||||||
{{ form.invoice_country.as_field_group }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">{% translate "Invoice Contact" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="form-body card-body">
|
|
||||||
<div class="row">
|
|
||||||
{{ form.invoice_email.as_field_group }}
|
|
||||||
{{ form.invoice_phone.as_field_group }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="col-sm-12 d-flex justify-content-end">
|
|
||||||
<button class="btn btn-primary me-1 mb-1" type="submit">{% translate "Create Organization" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,8 +65,8 @@
|
||||||
} else {
|
} else {
|
||||||
// No existing addresses found, a new address has to be entered.
|
// No existing addresses found, a new address has to be entered.
|
||||||
if (existingSection) existingSection.style.display = 'none'
|
if (existingSection) existingSection.style.display = 'none'
|
||||||
if (newSection) newSection.style.display = '' // Ensure newSection is not null
|
newSection.style.display = ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock card_content %}
|
||||||
|
|
|
@ -28,42 +28,38 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
{% for offering in service.offerings.all %}
|
||||||
{% for offering in service.offerings.all %}
|
<div class="card col-6 col-lg-3 col-md-4">
|
||||||
<div class="col-6 col-lg-3 col-md-4">
|
<div class="card-header d-flex align-items-center">
|
||||||
<div class="card">
|
{% if offering.provider.logo %}
|
||||||
<div class="card-header d-flex align-items-center">
|
<img src="{{ offering.provider.logo.url }}"
|
||||||
{% if offering.provider.logo %}
|
alt="{{ offering.provider.name }}"
|
||||||
<img src="{{ offering.provider.logo.url }}"
|
class="me-3"
|
||||||
alt="{{ offering.provider.name }}"
|
style="max-width: 48px;
|
||||||
class="me-3"
|
max-height: 48px">
|
||||||
style="max-width: 48px;
|
{% endif %}
|
||||||
max-height: 48px">
|
<div class="d-flex flex-column">
|
||||||
{% endif %}
|
<h4 class="mb-0">{{ offering.provider.name }}</h4>
|
||||||
<div class="d-flex flex-column">
|
|
||||||
<h4 class="mb-0">{{ offering.provider.name }}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if offering.description %}
|
|
||||||
<p class="card-text">{{ offering.description }}</p>
|
|
||||||
{% elif offering.provider.description %}
|
|
||||||
<p class="card-text">{{ offering.provider.description }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-footer d-flex justify-content-between">
|
|
||||||
<span></span>
|
|
||||||
<a href="offering/{{ offering.pk }}/" class="btn btn-light-primary">{% translate "Read More" %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
<div class="card-body">
|
||||||
<div class="card">
|
{% if offering.description %}
|
||||||
<div class="card-body">
|
<p class="card-text">{{ offering.description }}</p>
|
||||||
<p>{% translate "No offerings found." %}</p>
|
{% elif offering.provider.description %}
|
||||||
</div>
|
<p class="card-text">{{ offering.provider.description }}</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="card-footer d-flex justify-content-between">
|
||||||
</div>
|
<span></span>
|
||||||
|
<a href="offering/{{ offering.pk }}/" class="btn btn-light-primary">{% translate "Read More" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>{% translate "No offerings found." %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -16,40 +16,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
{% for service in services %}
|
||||||
{% for service in services %}
|
<div class="card col-6 col-lg-3 col-md-4">
|
||||||
<div class="col-6 col-lg-3 col-md-4">
|
<div class="card-header d-flex align-items-center">
|
||||||
<div class="card">
|
{% if service.logo %}
|
||||||
<div class="card-header d-flex align-items-center">
|
<img src="{{ service.logo.url }}"
|
||||||
{% if service.logo %}
|
alt="{{ service.name }}"
|
||||||
<img src="{{ service.logo.url }}"
|
class="me-3"
|
||||||
alt="{{ service.name }}"
|
style="max-width: 48px;
|
||||||
class="me-3"
|
max-height: 48px">
|
||||||
style="max-width: 48px;
|
{% endif %}
|
||||||
max-height: 48px">
|
<div class="d-flex flex-column">
|
||||||
{% endif %}
|
<h4 class="mb-0">{{ service.name }}</h4>
|
||||||
<div class="d-flex flex-column">
|
<small class="text-muted">{{ service.category }}</small>
|
||||||
<h4 class="mb-0">{{ service.name }}</h4>
|
|
||||||
<small class="text-muted">{{ service.category }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if service.description %}<p class="card-text">{{ service.description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-footer d-flex justify-content-between">
|
|
||||||
<span></span>
|
|
||||||
<a href="{{ service.slug }}/" class="btn btn-light-primary">{% translate "Read More" %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
<div class="card-body">
|
||||||
<div class="card">
|
{% if service.description %}<p class="card-text">{{ service.description }}</p>{% endif %}
|
||||||
<div class="card-body">
|
|
||||||
<p>{% translate "No services found." %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="card-footer d-flex justify-content-between">
|
||||||
</div>
|
<span></span>
|
||||||
|
<a href="{{ service.slug }}/" class="btn btn-light-primary">{% translate "Read More" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>{% translate "No services found." %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
<script src="{% static "js/autosubmit.js" %}" defer></script>
|
<script src="{% static "js/autosubmit.js" %}" defer></script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -36,105 +36,26 @@
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
{% endpartialdef org-name-edit %}
|
{% endpartialdef org-name-edit %}
|
||||||
{% block content %}
|
{% block card_content %}
|
||||||
<section class="section">
|
<div class="table-responsive">
|
||||||
<div class="card">
|
<table class="table table-lg">
|
||||||
<div class="card-content">
|
<tbody>
|
||||||
<div class="card-body">
|
<tr>
|
||||||
<div class="table-responsive">
|
<th class="w-25">
|
||||||
<table class="table table-lg">
|
<span class="d-flex mt-2">{% translate "Name" %}</span>
|
||||||
<tbody>
|
</th>
|
||||||
<tr>
|
{% partial org-name %}
|
||||||
<th class="w-25">
|
</tr>
|
||||||
<span class="d-flex mt-2">{% translate "Name" %}</span>
|
<tr>
|
||||||
</th>
|
<th class="w-25">
|
||||||
{% partial org-name %}
|
<span class="d-flex mt-2">{% translate "Namespace" %}</span>
|
||||||
</tr>
|
</th>
|
||||||
<tr>
|
<td>
|
||||||
<th class="w-25">
|
<div>{{ form.instance.namespace }}</div>
|
||||||
<span class="d-flex mt-2">{% translate "Namespace" %}</span>
|
<small class="text-muted">{% translate "System-generated namespace for Kubernetes resources." %}</small>
|
||||||
</th>
|
</td>
|
||||||
<td>
|
</tr>
|
||||||
<div>{{ form.instance.namespace }}</div>
|
</tbody>
|
||||||
<small class="text-muted">{% translate "System-generated namespace for Kubernetes resources." %}</small>
|
</table>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
{% endblock card_content %}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if form.instance.billing_entity and form.instance.billing_entity.odoo_data.invoice_address %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">{% translate "Billing Address" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="card-body">
|
|
||||||
{% with odoo_data=form.instance.billing_entity.odoo_data %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-lg">
|
|
||||||
<tbody>
|
|
||||||
{% if odoo_data.invoice_address %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "Invoice Contact Name" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.invoice_address.name|default:"" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "Street" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.invoice_address.street|default:"" }}</td>
|
|
||||||
</tr>
|
|
||||||
{% if odoo_data.invoice_address.street2 %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "Street 2" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.invoice_address.street2 }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "City" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.invoice_address.city|default:"" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "ZIP Code" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.invoice_address.zip|default:"" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "Country" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.invoice_address.country_id.1|default:"" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "VAT ID" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.company.vat|default:"" }}</td>
|
|
||||||
</tr>
|
|
||||||
<th class="w-25">
|
|
||||||
<span class="d-flex mt-2">{% translate "Invoice Email" %}</span>
|
|
||||||
</th>
|
|
||||||
<td>{{ odoo_data.invoice_address.email|default:"" }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
|
@ -22,14 +22,13 @@ class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView):
|
||||||
billing_choice = form.cleaned_data.get("billing_processing_choice")
|
billing_choice = form.cleaned_data.get("billing_processing_choice")
|
||||||
billing_entity = None
|
billing_entity = None
|
||||||
|
|
||||||
if not billing_choice or billing_choice == "new":
|
if billing_choice == "new":
|
||||||
billing_entity = BillingEntity.create_from_data(
|
billing_entity = BillingEntity.create_from_data(
|
||||||
form.cleaned_data["name"],
|
|
||||||
{
|
{
|
||||||
key: value
|
key[3:]: value
|
||||||
for key, value in form.cleaned_data.items()
|
for key, value in form.cleaned_data.items()
|
||||||
if key.startswith("invoice_")
|
if key.startswith("ba_")
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
elif odoo_id := form.cleaned_data.get("existing_odoo_address_id"):
|
elif odoo_id := form.cleaned_data.get("existing_odoo_address_id"):
|
||||||
billing_entity = BillingEntity.objects.filter(
|
billing_entity = BillingEntity.objects.filter(
|
||||||
|
|
|
@ -116,10 +116,7 @@ if all(
|
||||||
"addressing_style": SERVALA_S3_ADDRESSING_STYLE,
|
"addressing_style": SERVALA_S3_ADDRESSING_STYLE,
|
||||||
"signature_version": SERVALA_S3_SIGNATURE_VERSION,
|
"signature_version": SERVALA_S3_SIGNATURE_VERSION,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
"staticfiles": {
|
|
||||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ODOO = {
|
ODOO = {
|
||||||
|
|
8
uv.lock
generated
8
uv.lock
generated
|
@ -289,16 +289,16 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "5.2.1"
|
version = "5.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asgiref" },
|
{ name = "asgiref" },
|
||||||
{ name = "sqlparse" },
|
{ name = "sqlparse" },
|
||||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/10/0d546258772b8f31398e67c85e52c66ebc2b13a647193c3eef8ee433f1a8/django-5.2.1.tar.gz", hash = "sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284", size = 10818735, upload-time = "2025-05-07T14:06:17.543Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/1b/c6da718c65228eb3a7ff7ba6a32d8e80fa840ca9057490504e099e4dd1ef/Django-5.2.tar.gz", hash = "sha256:1a47f7a7a3d43ce64570d350e008d2949abe8c7e21737b351b6a1611277c6d89", size = 10824891, upload-time = "2025-04-02T13:08:06.874Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/92/7448697b5838b3a1c6e1d2d6a673e908d0398e84dc4f803a2ce11e7ffc0f/django-5.2.1-py3-none-any.whl", hash = "sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961", size = 8301833, upload-time = "2025-05-07T14:06:10.955Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/e0/6a5b5ea350c5bd63fe94b05e4c146c18facb51229d9dee42aa39f9fc2214/Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83", size = 8301361, upload-time = "2025-04-02T13:08:01.465Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1022,7 +1022,7 @@ dev = [
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "argon2-cffi", specifier = ">=23.1.0" },
|
{ name = "argon2-cffi", specifier = ">=23.1.0" },
|
||||||
{ name = "cryptography", specifier = ">=44.0.2" },
|
{ name = "cryptography", specifier = ">=44.0.2" },
|
||||||
{ name = "django", specifier = "==5.2.1" },
|
{ name = "django", specifier = "==5.2" },
|
||||||
{ name = "django-allauth", specifier = ">=65.5.0" },
|
{ name = "django-allauth", specifier = ">=65.5.0" },
|
||||||
{ name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" },
|
{ name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" },
|
||||||
{ name = "django-scopes", specifier = ">=2.0.0" },
|
{ name = "django-scopes", specifier = ">=2.0.0" },
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue