Skip to content

Commit

Permalink
👔 improvement: Add support for supplier invoice adjustments
Browse files Browse the repository at this point in the history
  • Loading branch information
kiloreven committed Jan 19, 2024
1 parent 048f2a8 commit 0f3bd47
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 38 deletions.
4 changes: 3 additions & 1 deletion oda_wd_client/base/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class WorkdayClient:
Relevant docs: https://community.workday.com/articles/628676
"""

_API_VERSION = "40.2"

# Each SOAP service must be instantiated individually, and we need more than one...
_services: list[str] = [
"Human_Resources",
Expand Down Expand Up @@ -109,7 +111,7 @@ def _init_service_client(self, service: str) -> None:
def _get_client_url(self, service: str) -> str:
return urljoin(
self._auth_base_url,
f"/ccx/service/{self._auth_tenant_name}/{service}/v38.1?wsdl",
f"/ccx/service/{self._auth_tenant_name}/{service}/v{self._API_VERSION}?wsdl",
)

def _setup_client(self, url: str) -> suds_client.Client:
Expand Down
30 changes: 29 additions & 1 deletion oda_wd_client/service/resource_management/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
from oda_wd_client.base.api import WorkdayClient
from oda_wd_client.base.tools import suds_to_dict
from oda_wd_client.service.resource_management.exceptions import NoSupplierID
from oda_wd_client.service.resource_management.types import Supplier, SupplierInvoice
from oda_wd_client.service.resource_management.types import (
Supplier,
SupplierInvoice,
SupplierInvoiceAdjustment,
)
from oda_wd_client.service.resource_management.utils import (
pydantic_supplier_invoice_to_workday,
workday_supplier_invoice_adjustment_to_pydantic,
workday_supplier_invoice_to_pydantic,
workday_supplier_to_pydantic,
)
Expand Down Expand Up @@ -64,3 +69,26 @@ def cancel_supplier_invoice(self, invoice_ref_id: str):
id_obj.value = invoice_ref_id
ref_obj.ID.append(id_obj)
return self._request(method, Supplier_Invoice_Reference=ref_obj)

def get_supplier_invoice_adjustments(
self, return_suds_object=False
) -> Iterator[sudsobject.Object | SupplierInvoiceAdjustment]:
method = "Get_Supplier_Invoice_Adjustments"
results = self._get_paginated(method, "Supplier_Invoice_Adjustment")
for invoice in results:
yield invoice if return_suds_object else workday_supplier_invoice_adjustment_to_pydantic(
suds_to_dict(invoice)
)

def submit_supplier_invoice_adjustment(
self, invoice: SupplierInvoiceAdjustment
) -> sudsobject.Object:
method = "Submit_Supplier_Invoice_Adjustment"
request = self.factory("ns0:Submit_Supplier_Invoice_Adjustment_Request")
request.Supplier_Invoice_Adjustment_Data = pydantic_supplier_invoice_to_workday(
invoice, self
)
return self._request(
method,
Supplier_Invoice_Adjustment_Data=request.Supplier_Invoice_Adjustment_Data,
)
52 changes: 45 additions & 7 deletions oda_wd_client/service/resource_management/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ class AdditionalReferenceType(WorkdayReferenceBaseModel):
] = "Additional_Reference_Type_ID"


class InvoiceAdjustmentReason(WorkdayReferenceBaseModel):
"""
Reference: https://community.workday.com/sites/default/files/file-hosting/productionapi/Resource_Management/v40.2/Submit_Supplier_Invoice_Adjustment.html#Invoice_Adjustment_ReasonObjectType # noqa
"""

_class_name = "Invoice_Adjustment_ReasonObject"
workday_id_type: Literal["Adjustment_Reason_ID"] = "Adjustment_Reason_ID"


class SupplierInvoiceLine(BaseModel):
"""
Reference: https://community.workday.com/sites/default/files/file-hosting/productionapi/Resource_Management/v40.2/Submit_Supplier_Invoice.html#Supplier_Invoice_Line_Replacement_DataType # noqa
Expand All @@ -184,27 +193,24 @@ class SupplierInvoiceLine(BaseModel):
budget_date: date | None


class SupplierInvoice(WorkdayReferenceBaseModel):
class BaseSupplierInvoice(WorkdayReferenceBaseModel):
"""
Reference: https://community.workday.com/sites/default/files/file-hosting/productionapi/Resource_Management/v40.2/Submit_Supplier_Invoice.html#Supplier_Invoice_DataType # noqa
Used as base class for SupplierInvoice and SupplierInvoiceAdjustment
Main reference: https://community.workday.com/sites/default/files/file-hosting/productionapi/Resource_Management/v40.2/Submit_Supplier_Invoice.html#Supplier_Invoice_DataType # noqa
"""

workday_id_type: Literal[
"Supplier_invoice_Reference_ID"
] = "Supplier_invoice_Reference_ID"
invoice_number: str | None
company: Company
currency: Currency
supplier: Supplier
invoice_date: date
due_date: date
total_amount: Decimal = Field(max_digits=26, decimal_places=6)
tax_amount: Decimal = Field(max_digits=26, decimal_places=6)
tax_option: TaxOption | None
additional_reference_number: str | None
additional_type_reference: AdditionalReferenceType | None
external_po_number: str | None
prepaid: bool = False
prepayment_release_type_reference: PrepaidAmortizationType | None = None

lines: list[SupplierInvoiceLine]
Expand All @@ -215,6 +221,38 @@ class SupplierInvoice(WorkdayReferenceBaseModel):
# Should not be edited inside Workday, only through API
locked_in_workday: bool = True


class SupplierInvoice(BaseSupplierInvoice):
"""
Reference: https://community.workday.com/sites/default/files/file-hosting/productionapi/Resource_Management/v40.2/Submit_Supplier_Invoice.html#Supplier_Invoice_DataType # noqa
"""

workday_id_type: Literal[
"Supplier_Invoice_Reference_ID"
] = "Supplier_Invoice_Reference_ID"

invoice_date: date
prepaid: bool = False

_normalize_dates = validator("invoice_date", "due_date", allow_reuse=True)(
parse_workday_date
)


class SupplierInvoiceAdjustment(BaseSupplierInvoice):
"""
Reference: https://community.workday.com/sites/default/files/file-hosting/productionapi/Resource_Management/v40.2/Submit_Supplier_Invoice_Adjustment.html#Supplier_Invoice_Adjustment_DataType # noqa
"""

workday_id_type: Literal[
"Supplier_Invoice_Adjustment_Reference_ID"
] = "Supplier_Invoice_Adjustment_Reference_ID"

adjustment_date: date
adjustment_reason: InvoiceAdjustmentReason = InvoiceAdjustmentReason(
workday_id="Other_Terms"
)

_normalize_dates = validator("adjustment_date", "due_date", allow_reuse=True)(
parse_workday_date
)
114 changes: 86 additions & 28 deletions oda_wd_client/service/resource_management/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
PrepaidAmortizationType,
Supplier,
SupplierInvoice,
SupplierInvoiceAdjustment,
SupplierInvoiceLine,
SupplierStatus,
TaxApplicability,
Expand Down Expand Up @@ -126,7 +127,7 @@ def workday_supplier_to_pydantic(data: dict) -> Supplier:
def _workday_invoice_line_to_pydantic(data: dict, order: int) -> SupplierInvoiceLine:
cost_center = None
# Worktags is a list of tags, each with their own list of IDs
worktags = data["Worktags_Reference"]
worktags = data.get("Worktags_Reference", [])
for tag in worktags:
_cost_center = CostCenterWorktag.from_id_list(tag["ID"])
if _cost_center:
Expand Down Expand Up @@ -157,21 +158,23 @@ def _workday_invoice_line_to_pydantic(data: dict, order: int) -> SupplierInvoice
tax_rate_options_data=tax_rate_options_data,
tax_applicability=TaxApplicability.from_id_list(
data["Tax_Applicability_Reference"]["ID"]
),
tax_code=TaxCode.from_id_list(data["Tax_Code_Reference"]["ID"]),
)
if "Tax_Applicability_Reference" in data
else None,
tax_code=TaxCode.from_id_list(data["Tax_Code_Reference"]["ID"])
if "Tax_Code_reference" in data
else None,
spend_category=SpendCategory.from_id_list(
data["Spend_Category_Reference"]["ID"]
),
)
if "Spend_Category_Reference" in data
else None,
cost_center=cost_center,
gross_amount=data["Extended_Amount"],
)


def workday_supplier_invoice_to_pydantic(data: dict) -> SupplierInvoice:
data_list = data["Supplier_Invoice_Data"]
assert len(data_list) == 1, "Expecting only one invoice in this dataset"
inv: dict[str, Any] = data_list[0]

def _get_common_invoice_attributes(inv: dict) -> dict:
lines = []
for i, line in enumerate(
sorted(
Expand All @@ -185,23 +188,39 @@ def workday_supplier_invoice_to_pydantic(data: dict) -> SupplierInvoice:
)
currency_ref = get_id_from_list(inv["Currency_Reference"]["ID"], "Currency_ID")
supplier_ref = get_id_from_list(inv["Supplier_Reference"]["ID"], "Supplier_ID")
prepaid_ref = get_id_from_list(
inv["Prepayment_Release_Type_Reference"]["ID"], "Prepayment_Release_Type_ID"
)

# Type narrowing
assert company_ref is not None
assert currency_ref is not None
assert supplier_ref is not None

return {
"company_ref": company_ref,
"currency_ref": currency_ref,
"supplier_ref": supplier_ref,
"lines": lines,
}


def workday_supplier_invoice_to_pydantic(data: dict) -> SupplierInvoice:
data_list = data["Supplier_Invoice_Data"]
assert len(data_list) == 1, "Expecting only one invoice in this dataset"
inv: dict[str, Any] = data_list[0]

prepaid_ref = get_id_from_list(
inv["Prepayment_Release_Type_Reference"]["ID"], "Prepayment_Release_Type_ID"
)
common = _get_common_invoice_attributes(inv)

return SupplierInvoice(
workday_id=get_id_from_list(
data["Supplier_Invoice_Reference"]["ID"], "Supplier_Invoice_Reference_ID"
data["Supplier_Invoice_Reference"]["ID"],
id_type="Supplier_Invoice_Reference_ID",
),
invoice_number=inv["Invoice_Number"],
company=Company(workday_id=company_ref),
currency=Currency(currency_code=currency_ref),
supplier=Supplier(workday_id=supplier_ref),
company=Company(workday_id=common["company_ref"]),
currency=Currency(currency_code=common["currency_ref"]),
supplier=Supplier(workday_id=common["supplier_ref"]),
invoice_date=inv["Invoice_Date"],
due_date=inv["Due_Date_Override"],
total_amount=inv["Control_Amount_Total"],
Expand All @@ -212,7 +231,33 @@ def workday_supplier_invoice_to_pydantic(data: dict) -> SupplierInvoice:
)
if prepaid_ref
else None,
lines=lines,
lines=common["lines"],
)


def workday_supplier_invoice_adjustment_to_pydantic(
data: dict,
) -> SupplierInvoiceAdjustment:
data_list = data["Supplier_Invoice_Adjustment_Data"]
assert len(data_list) == 1, "Expecting only one invoice in this dataset"
inv: dict[str, Any] = data_list[0]

common = _get_common_invoice_attributes(inv)

return SupplierInvoiceAdjustment(
workday_id=get_id_from_list(
data["Supplier_Invoice_Adjustment_Reference"]["ID"],
id_type="Supplier_Invoice_Adjustment_Reference_ID",
),
invoice_number=inv["Invoice_Number"],
company=Company(workday_id=common["company_ref"]),
currency=Currency(currency_code=common["currency_ref"]),
supplier=Supplier(workday_id=common["supplier_ref"]),
adjustment_date=inv["Adjustment_Date"],
due_date=inv["Due_Date_Override"],
total_amount=inv["Control_Total_Amount"],
tax_amount=inv["Tax_Amount"],
lines=common["lines"],
)


Expand Down Expand Up @@ -260,12 +305,33 @@ def _get_wd_invoice_lines_from_invoice(


def pydantic_supplier_invoice_to_workday(
invoice: SupplierInvoice, client: WorkdayClient
invoice: SupplierInvoice | SupplierInvoiceAdjustment, client: WorkdayClient
) -> sudsobject.Object:
"""
Generate the data that is needed for a for Supplier_Invoice_Data in a call to Submit_Supplier_Invoice
Generate the data that is needed for a for Supplier_Invoice_Data in a call to Submit_Supplier_Invoice,
or for Supplier_Invoice_Adjustment_Data in a call to Submit_Supplier_Invoice_Adjustment
"""
invoice_data = client.factory("ns0:Supplier_Invoice_DataType")

# Handle attributes unique to a specific type
if isinstance(invoice, SupplierInvoice):
invoice_data = client.factory("ns0:Supplier_Invoice_DataType")
invoice_data.Invoice_Date = str(invoice.invoice_date)
invoice_data.Control_Amount_Total = invoice.total_amount
invoice_data.Prepaid = invoice.prepaid
invoice_data.Prepayment_Release_Type_Reference = (
(invoice.prepayment_release_type_reference.wd_object(client))
if invoice.prepayment_release_type_reference
else None
)
elif isinstance(invoice, SupplierInvoiceAdjustment):
invoice_data = client.factory("ns0:Supplier_Invoice_Adjustment_DataType")
invoice_data.Adjustment_Date = str(invoice.adjustment_date)
invoice_data.Control_Total_Amount = invoice.total_amount
invoice_data.Adjustment_Reason_Reference = invoice.adjustment_reason.wd_object(
client
)
else:
raise Exception("Invalid invoice type")

invoice_data.Submit = invoice.submit
invoice_data.Locked_in_Workday = invoice.locked_in_workday
Expand All @@ -278,9 +344,7 @@ def pydantic_supplier_invoice_to_workday(
invoice_data.Supplier_Reference = invoice.supplier.wd_object(client)
if invoice.tax_option:
invoice_data.Default_Tax_Option_Reference = invoice.tax_option.wd_object(client)
invoice_data.Invoice_Date = str(invoice.invoice_date)
invoice_data.Due_Date_Override = str(invoice.due_date)
invoice_data.Control_Amount_Total = invoice.total_amount
# invoice_data.Tax_Amount = invoice.tax_amount
# invoice_data.Attachment_Data = _get_wd_attachment_data_from_invoice(invoice)
invoice_data.Invoice_Line_Replacement_Data = _get_wd_invoice_lines_from_invoice(
Expand All @@ -299,12 +363,6 @@ def pydantic_supplier_invoice_to_workday(
if invoice.external_po_number:
invoice_data.External_PO_Number = invoice.external_po_number

invoice_data.Prepaid = invoice.prepaid
invoice_data.Prepayment_Release_Type_Reference = (
(invoice.prepayment_release_type_reference.wd_object(client))
if invoice.prepayment_release_type_reference
else None
)
if invoice.attachments:
invoice_data.Attachment_Data = [
attachment.wd_object(client) for attachment in invoice.attachments
Expand Down
2 changes: 2 additions & 0 deletions oda_wd_client/types/resource_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
SpendCategory,
Supplier,
SupplierInvoice,
SupplierInvoiceAdjustment,
SupplierInvoiceLine,
TaxApplicability,
TaxCode,
Expand All @@ -18,6 +19,7 @@
"SpendCategory",
"Supplier",
"SupplierInvoice",
"SupplierInvoiceAdjustment",
"SupplierInvoiceLine",
"TaxApplicability",
"TaxCode",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "oda-wd-client"
version = "0.0.18"
version = "0.0.19a0"
description = "A library for interacting with Workday from Python"
authors = [
"Karl Fredrik Haugland <[email protected]>",
Expand Down

0 comments on commit 0f3bd47

Please sign in to comment.