diff --git a/src/saleorder_storage.go b/src/saleorder_storage.go new file mode 100644 index 0000000..5d01a2f --- /dev/null +++ b/src/saleorder_storage.go @@ -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:]) +} diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index e610572..80e2d9e 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -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: