Implement Sale Order creation on org creation

This commit is contained in:
Tobias Kunze 2025-06-15 18:00:18 +02:00
parent 2579aff765
commit 5b7c26bbac
2 changed files with 186 additions and 0 deletions

152
src/saleorder_storage.go Normal file
View file

@ -0,0 +1,152 @@
package saleorder
import (
"fmt"
"strconv"
"strings"
organizationv1 "github.com/appuio/control-api/apis/organization/v1"
odooclient "github.com/appuio/go-odoo"
)
type Odoo16Credentials = odooclient.ClientConfig
type Odoo16Options struct {
SaleOrderClientReferencePrefix string
SaleOrderInternalNote string
Odoo8CompatibilityMode bool
}
const defaultSaleOrderState = "sale"
type SaleOrderStorage interface {
CreateSaleOrder(organizationv1.Organization) (string, error)
GetSaleOrderName(organizationv1.Organization) (string, error)
}
type Odoo16Client interface {
Read(string, []int64, *odooclient.Options, interface{}) error
CreateSaleOrder(*odooclient.SaleOrder) (int64, error)
FindResPartners(*odooclient.Criteria, *odooclient.Options) (*odooclient.ResPartners, error)
}
type Odoo16SaleOrderStorage struct {
client Odoo16Client
options *Odoo16Options
}
func NewOdoo16Storage(credentials *Odoo16Credentials, options *Odoo16Options) (SaleOrderStorage, error) {
client, err := odooclient.NewClient(credentials)
return &Odoo16SaleOrderStorage{
client: client,
options: options,
}, err
}
func NewOdoo16StorageFromClient(client Odoo16Client, options *Odoo16Options) SaleOrderStorage {
return &Odoo16SaleOrderStorage{
client: client,
options: options,
}
}
func (s *Odoo16SaleOrderStorage) CreateSaleOrder(org organizationv1.Organization) (string, error) {
beID, err := k8sIDToOdooID(org.Spec.BillingEntityRef)
if err != nil {
return "", err
}
var beRecord odooclient.ResPartner
fetchPartnerFieldOpts := odooclient.NewOptions().FetchFields(
"id",
"parent_id",
)
if s.options.Odoo8CompatibilityMode {
odoo8ID := fmt.Sprintf("__export__.res_partner_%d", beID)
idMatchCriteria := odooclient.NewCriteria().Add("x_odoo_8_ID", "=", odoo8ID)
r, err := s.client.FindResPartners(idMatchCriteria, fetchPartnerFieldOpts)
if err != nil {
return "", fmt.Errorf("fetching accounting contact by ID: %w", err)
}
if len(*r) <= 0 {
return "", fmt.Errorf("no results when fetching accounting contact by ID")
}
resPartners := *r
beRecord = resPartners[0]
} else {
beRecords := []odooclient.ResPartner{}
err = s.client.Read(odooclient.ResPartnerModel, []int64{int64(beID)}, fetchPartnerFieldOpts, &beRecords)
if err != nil {
return "", fmt.Errorf("fetching accounting contact by ID: %w", err)
}
if len(beRecords) <= 0 {
return "", fmt.Errorf("no results when fetching accounting contact by ID")
}
beRecord = beRecords[0]
}
if beRecord.ParentId == nil {
return "", fmt.Errorf("accounting contact %d has no parent", beRecord.Id.Get())
}
var clientRef string
if org.Spec.DisplayName != "" {
clientRef = fmt.Sprintf("%s (%s)", s.options.SaleOrderClientReferencePrefix, org.Spec.DisplayName)
} else {
clientRef = fmt.Sprintf("%s (%s)", s.options.SaleOrderClientReferencePrefix, org.ObjectMeta.Name)
}
newSaleOrder := odooclient.SaleOrder{
PartnerInvoiceId: odooclient.NewMany2One(beRecord.Id.Get(), ""),
PartnerId: odooclient.NewMany2One(beRecord.ParentId.ID, ""),
State: odooclient.NewSelection(defaultSaleOrderState),
ClientOrderRef: odooclient.NewString(clientRef),
InternalNote: odooclient.NewString(s.options.SaleOrderInternalNote),
}
soID, err := s.client.CreateSaleOrder(&newSaleOrder)
if err != nil {
return "", fmt.Errorf("creating new sale order: %w", err)
}
return fmt.Sprint(soID), nil
}
func (s *Odoo16SaleOrderStorage) GetSaleOrderName(org organizationv1.Organization) (string, error) {
fetchOrderFieldOpts := odooclient.NewOptions().FetchFields(
"id",
"name",
)
id, err := strconv.Atoi(org.Status.SalesOrderID)
if err != nil {
return "", fmt.Errorf("error parsing saleOrderID %q from organization status: %w", org.Status.SalesOrderID, err)
}
soRecords := []odooclient.SaleOrder{}
err = s.client.Read(odooclient.SaleOrderModel, []int64{int64(id)}, fetchOrderFieldOpts, &soRecords)
if err != nil {
return "", fmt.Errorf("fetching sale order by ID: %w", err)
}
if len(soRecords) <= 0 {
return "", fmt.Errorf("no results when fetching sale orders with ID %q", id)
}
return soRecords[0].Name.Get(), nil
}
func k8sIDToOdooID(id string) (int, error) {
if !strings.HasPrefix(id, "be-") {
return 0, fmt.Errorf("invalid ID, missing prefix: %s", id)
}
return strconv.Atoi(id[3:])
}

View file

@ -46,6 +46,13 @@ class Organization(ServalaModelMixin, models.Model):
verbose_name=_("Members"),
)
odoo_sale_order_id = models.IntegerField(
null=True, blank=True, verbose_name=_("Odoo Sale Order ID")
)
odoo_sale_order_name = models.CharField(
max_length=100, null=True, blank=True, verbose_name=_("Odoo Sale Order Name")
)
class urls(urlman.Urls):
base = "/org/{self.slug}/"
details = "{base}details/"
@ -67,6 +74,7 @@ class Organization(ServalaModelMixin, models.Model):
)
@classmethod
@transaction.atomic
def create_organization(cls, instance, owner):
try:
instance.origin
@ -76,6 +84,32 @@ class Organization(ServalaModelMixin, models.Model):
)
instance.save()
instance.set_owner(owner)
if (
instance.billing_entity.odoo_company_id
and instance.billing_entity.odoo_invoice_id
):
payload = {
"partner_id": instance.billing_entity.odoo_company_id,
"partner_invoice_id": instance.billing_entity.odoo_invoice_id,
"state": "sale",
"client_order_ref": f"Servala (Organization: {instance.name})",
"note": "auto-generated by Servala Portal",
}
sale_order_id = CLIENT.execute("sale.order", "create", [payload])
sale_order_data = CLIENT.search_read(
model="sale.order",
domain=[["id", "=", sale_order_id]],
fields=["name"],
limit=1,
)
instance.odoo_sale_order_id = sale_order_id
if sale_order_data:
instance.odoo_sale_order_name = sale_order_data[0]["name"]
instance.save(update_fields=["odoo_sale_order_id", "odoo_sale_order_name"])
return instance
class Meta: