Invoice reads through IExternalErpClient return the provider-neutral DevOp.ERP.Models.SalesInvoice; IDkPlusClient returns the sealed DkPlusSalesInvoice subtype, whose lines are DkPlusSalesDocumentLine and which adds the DK Plus order, settlement, claim, dimension, and receiver data (see Models and writes). DK Plus invoice creation and price calculation use SalesInvoiceCreateRequest, because the v1 write contract differs materially from the read model and has provider-specific options. A read model is never accepted as write input.
var invoice = await client.Sales.Invoice.GetAsync("INV-100", cancellationToken);
var pdf = await client.Sales.Invoice.GetPdfAsync("INV-100", cancellationToken: cancellationToken);
var html = await client.Sales.Invoice.GetHtmlAsync("INV-100", cancellationToken: cancellationToken);
GetPdfAsync returns PDF bytes and rejects a successful response that does not have a PDF signature. GetHtmlAsync returns the raw HTML supplied by DK Plus. Both accept the optional DK Plus custom template value.
The CLI writes downloads atomically and refuses to replace existing files unless --force is supplied:
devop-dk sales invoice pdf INV-100 --output ./INV-100.pdf
devop-dk sales invoice html INV-100 --output ./INV-100.html
Invoice, order, and quote email operations share the public SalesDocumentEmailMessage request model:
await client.Sales.Invoice.SendEmailAsync(
"INV-100",
new SalesDocumentEmailMessage
{
To = "[email protected]",
Subject = "Invoice INV-100",
Body = "Please find the invoice attached.",
Priority = SalesDocumentEmailPriority.Normal
},
background: true,
cancellationToken: cancellationToken);
Email is externally visible and is never retried automatically. The CLI requires --yes before it calls the endpoint:
devop-dk sales invoice email INV-100 --to [email protected] --yes
Sales orders and quotes use their numeric document number for the v1 document endpoints. Their ordinary GetAsync methods continue to use the v2 UUID identifier.
var orderPdf = await client.Sales.Order.GetPdfAsync(123, cancellationToken: cancellationToken);
var orderHtml = await client.Sales.Order.GetHtmlAsync(123, cancellationToken: cancellationToken);
var quotePdf = await client.Sales.Quote.GetPdfAsync(456, cancellationToken);
var quoteHtml = await client.Sales.Quote.GetHtmlAsync(456, cancellationToken);
Orders accept the optional DK Plus custom template value. The quote endpoints do not define a custom-template parameter. Both document types use SalesDocumentEmailMessage for email:
await client.Sales.Order.SendEmailAsync(
123,
new SalesDocumentEmailMessage { To = "[email protected]" },
custom: "standard",
cancellationToken);
await client.Sales.Quote.SendEmailAsync(
456,
new SalesDocumentEmailMessage { To = "[email protected]" },
cancellationToken);
The equivalent CLI commands are sales order pdf|html|email and sales quote pdf|html|email. Every email command requires --yes because it causes externally visible delivery.
DK Plus ignores Lines in the head body of a v2 order, quote, or recurring sale, so since 0.4.0 the client writes the head and the lines as separate requests. Signatures are unchanged.
CreateAsync with lines:
null line is rejected as Lines[i].Lines.{uid}/Line in request order.If a line fails or the operation is canceled after the head exists, the client deletes the new head and rethrows the original exception (an AggregateException if that delete also fails). If the request supplied the document Number, the head is never deleted, because DK Plus may have matched an existing document; the exception message says so.
UpdateAsync with lines sends the head PUT without Lines, then each line in request order: PUT {uid}/Line/{sequence} for a line with a Sequence, POST {uid}/Line for a line without one. Stored lines that the request does not list are kept; remove a line with DeleteLineAsync. An update that sets only Lines sends no head PUT, and Lines = [] sends nothing. Nothing is rolled back: if a line fails, the head and earlier lines stay applied, and the exception message names the failed line and says so. Whether a line PUT replaces the stored line as intended is unverified live.
Quote lines cannot store QuantityDelivered, OrderedQuantity, DailyRate, or IndexRatio; a quote create or update (or a single-line create or update) that sets one is rejected with DkPlusModelValidationException before anything is sent. Orders and recurring sales still accept them.
Known DK Plus issue with quotes. DK Plus can create a v2 quote head with Id 0 and no Number; its line endpoint then fails and the head cannot be deleted. Sales.Quote.CreateAsync with lines therefore stops after the head, sends no line and no delete, and throws InvalidOperationException naming the UId and stating that the lines were not added and the head may not be deletable. A quote created without lines is unaffected. Order creates with lines work.
Sales.Quote.DeleteAsync uses the route declared in the DK Plus Swagger document, DELETE /api/v2/sales/quote/api/v2/sales/quote/{uid} (the plain {uid} route answered HTTP 405). The Swagger route has not been confirmed against a live company.
using DevOp.DK.Plus.Models;
var request = new SalesInvoiceCreateRequest
{
Customer = new DkPlusSalesDocumentParty { Number = "C-100" },
SalesPerson = "S-1",
Reference = "WEB-1042",
Date = DateTimeOffset.UtcNow,
Lines =
[
new SalesInvoiceLineRequest
{
ItemCode = "ITEM-1",
Quantity = 2,
PricesIncludeTax = false
}
]
};
var calculated = await client.Sales.Invoice.CalculatePricesAsync(
request,
cancellationToken);
Create only after the application has reviewed the calculated result and selected the desired posting behavior:
var created = await client.Sales.Invoice.CreateAsync(
request,
post: false,
cancellationToken);
CreateAsync is a remote mutation. The client does not retry it automatically. Application code should provide its own idempotency or reconciliation policy before retrying an ambiguous failure.
Attachments use FileAttachment publicly and are mapped internally to the DK Plus { Name, Content } schema. Public code never depends on generated Swagger types.
The related request types are SalesInvoiceLineRequest, SalesInvoicePayment, SalesInvoiceCreateOptions, SalesDocumentContact, SalesDocumentLineVariation, and the SalesInvoiceSaleType enum (Debit, Credit, ReturnSale, DebitComputation, CreditComputation). The customer and receiver are DkPlusSalesDocumentParty values. The v1 invoice head cannot store every party and variation member: a customer ContactName, MobilePhone, or address City, a receiver NationalId or Email, any party or contact AdditionalData, a contact Address, and a variation SecondaryDescription or QuantityOnBackOrders are rejected with a DkPlusModelValidationException before the request is sent. The customer accepts up to four address lines and the receiver three. For v2 orders, quotes, and recurring sales, the contact maps its Address (three lines, postal code, country; a City or fourth line is rejected before sending), the receiver is a DkPlusSalesDocumentParty, and the party, contact, carrier, and extra-details objects keep unknown members in their own AdditionalData. Requests accept DiscountAmount and a line Sequence; see the section above for how lines are written.