- Most endpoints (outside codeunits) allow ODataV4
- Codeunits must be done through SOAP
- ODataV4 in NAV does support $top, $count, and $skip
- NAV has a concept of "pages" - basically a class. This for CRUD operations. Codeunits would be where you hit extensibility stuff
- https://docs.microsoft.com/en-us/dynamics-nav/using-filter-expressions-in-odata-uris
- Setting up web services:
- You search "Services"
- You get to a page with a bunch of ODataV4 and SOAP URLs
- You can expose any page as a new service
- Generating classes from an XML:
dotnet o2pgen -r MyWSDL.xml -m "C4.Modules.DynNAV18" -c pas -C pas -k -a json3
- Recommend OData.QueryBuilder over Microsoft's library
- Orders become invoices (possibly)
- Items have a "Sellable" flag
- Images are stored by their URL
- Every account has a CustomerDiscGroup
- If you get an error about some kind of invalid data when doing a list request, it's a data issue already in the client's instance. You need to march one-by-one through that page using $skip and $top and ignore the element that finally errors.
- Must send the ETag as a header in
If-Match
- Inventory comes back directly on the Product/Item object
foreach (var item in lineItems.Where(item => item.Type == "Item"))
{
var lineItem = Mapper.Map<SalesItemBaseModel<IAppliedSalesInvoiceItemDiscountModel, AppliedSalesInvoiceItemDiscountModel>>(item);
lineItem.SetSerializableAttributesFromJsonOf(item, _navOpts.FieldsToSyncAsAttributes);
cefInvoice.SalesItems.Add(lineItem);
}
CreateMap<NAVOrderModel, SalesOrderModel>()
.ForMember(d => d.CustomKey, o => o.MapFrom(s => s.CustomKey))
// Ugly, but it reduces code duplication
.ForMember(d => d.AccountKey, o => o.MapFrom(s => new NAVAccountModel{ Number = s.SellToCustomerNo }.CustomKey))
// Note: PostingDate seems to always be an invalid date in NAV
.ForMember(d => d.CreatedDate, o => o.MapFrom(s => s.OrderDate.DateTime))
.ForMember(d => d.UpdatedDate, o => o.MapFrom(s => s.DocumentDate.DateTime))
// Now that's a beautifully simple mapping
.ForMember(d => d.DueDate, o => o.MapFrom(s => s.DueDate.DateTime))
// Hardcoded to mean it was placed through the Web
.ForMember(d => d.TypeID, o => o.MapFrom(s => 1))
// Hardcoded 1 for WORK
.ForMember(d => d.StateID, o => o.MapFrom(s => 1))
// It's always either Open or Released.
.ForMember(d => d.StatusID, o => o.MapFrom(s => s.Status == "Open" ? FullPaymentReceivedStatusID : ProcessingStatusID))
.ForMember(d => d.UserKey, o => o.MapFrom(s => s.SellToCustomerNo))
//.ForMember(d => d.BalanceDue, o => o.MapFrom(s => s.))
.ForMember(d => d.OrderApprovedDate, o => o.MapFrom(s => s.PostingDate.DateTime))
CreateMap<SalesOrderModel, NAVOrderModel>()
.BeforeMap((s, d) =>
{
// Pre-populate with anything that was already in the previous NAV order
d.SetMembersFromSerializableAttributesOf(s);
})
.ForMember(d => d.Number, o => o.MapFrom(s => s.GetSerializedNumberOrGenerate()))
.ForMember(d => d.OrderDate, o => o.MapFrom(s => new DateTimeOffset(s.CreatedDate)))
.ForMember(d => d.DueDate, o => o.MapFrom(s => new DateTimeOffset(s.DueDate ?? s.CreatedDate.AddDays(30))))
.ForMember(d => d.DocumentDate, o => o.MapFrom(s => new DateTimeOffset(s.CreatedDate)))
// TODO
.ForMember(d => d.TaxAreaCode, o => o.MapFrom(s => "AVATAX"))
// Billing
.ForMember(d => d.SellToCustomerNo, o => o.MapFrom(s => s.AccountKey.MakeSafeForNAVNumber()))
.ForMember(d => d.SellToCustomerName, o => o.MapFrom(s => s.AccountName))
.ForMember(d => d.SellToAddress, o => o.MapFrom(s => s.BillingContact.Address.Street1))
.ForMember(d => d.SellToAddress2, o => o.MapFrom(s => s.BillingContact.Address.Street2))
.ForMember(d => d.SellToCity, o => o.MapFrom(s => s.BillingContact.Address.City))
.ForMember(d => d.SellToCounty, o => o.MapFrom(s => s.BillingContact.Address.RegionName))
.ForMember(d => d.SellToPostCode, o => o.MapFrom(s => s.BillingContact.Address.PostalCode))
// Billing part 2
.ForMember(d => d.BillToAddress, o => o.MapFrom(s => s.BillingContact.Address.Street1))
.ForMember(d => d.BillToAddress2, o => o.MapFrom(s => s.BillingContact.Address.Street2))
.ForMember(d => d.BillToCity, o => o.MapFrom(s => s.BillingContact.Address.City))
.ForMember(d => d.BillToCounty, o => o.MapFrom(s => s.BillingContact.Address.RegionName))
.ForMember(d => d.BillToPostCode, o => o.MapFrom(s => s.BillingContact.Address.PostalCode))
// Shipping
.ForMember(d => d.ShipToAddress, o => o.MapFrom(s => s.ShippingContact.Address.Street1))
.ForMember(d => d.ShipToAddress2, o => o.MapFrom(s => s.ShippingContact.Address.Street2))
.ForMember(d => d.ShipToCity, o => o.MapFrom(s => s.ShippingContact.Address.City))
.ForMember(d => d.ShipToCounty, o => o.MapFrom(s => s.ShippingContact.Address.RegionName))
.ForMember(d => d.ShipToPostCode, o => o.MapFrom(s => s.ShippingContact.Address.PostalCode))
// TODO: More thorough
.ForMember(d => d.Status, o => o.MapFrom(s => s.StatusID == FullPaymentReceivedStatusID ? "Open" : "Released" ))
.ForMember(d => d.TaxLiable, o => o.MapFrom(s => s.Totals.Tax > 0))
// Cannot map s.Totals to anything here.
.ForAllOtherMembers(o => o.Ignore());
foreach (var (index, originalOrder) in cefOrders.WithProgress(bar).Select((p, i) => (i, p)))
{
var navOrder = Mapper.Map<NAVOrderModel>(originalOrder);
if (await _navService.ReadOrder(navOrder, token) != null) continue;
context.WriteLine($" Syncing {navOrder.Number}");
await _navService.CreateOrder(navOrder, token);
// SalesItems aren't returned by a list. Need to re-read
var cefOrder = await _cefService.ReadSalesOrderById(originalOrder.ID.Value, token);
foreach (var (i, item) in cefOrder.SalesItems.Select((i, item) => (item, i)))
{
var navItem = Mapper.Map<NAVOrderLineItemModel>(item);
navItem.DocumentNumber = navOrder.Number;
navItem.LineNumber = i + 1;
await _navService.CreateOrderLineItem(navItem, token);
}
// Make sure we send back the new CustomKey
if (string.IsNullOrEmpty(originalOrder.CustomKey))
{
cefOrder.CustomKey = new NAVOrderModel { Number = cefOrder.GetSerializedNumberOrGenerate() }.CustomKey;
await _cefService.UpsertSalesOrder(cefOrder, token);
}
}
- These will be your main navigation options

- Click on Sales in the bottom left followed by Items


- Click on Finance in the bottom left, and then Customers


- Click on the Sales in the bottom left, and then the Sales Orders

