I am using the Xero REST API to manage some invoice creation/collation.
When updating an invoice with a POST request, the LineItems
inside the invoice seem to order themselves to their own ordering system.
I have double checked the order of my LineItems
list before running jsonEncode
and submitting, but the end result on Xero is always the same, the invoice LineItem
entries are randomly sorted in a way I cannot understand.
From what I understand the jsonEncode
should not affect the ordering of the items, so it isn’t that. I cannot ask Xero as they refuse to answer any code related questions.
Here is the code I am using:
Future updateOrCreateMonthlyInvoice(String contactName, List<DocketData> dockets, int conNumber, String collectedFrom) async {
await _checkToken();
var encodedContactName = Uri.encodeComponent(contactName);
var invoiceGet = await http.get(Uri.parse('https://api.xero.com/api.xro/2.0/invoices?page=1&where=Status=="DRAFT" AND Contact.Name=="$encodedContactName"'),
headers: restHeader);
Map<String, dynamic> invoiceData;
List<Map<String, dynamic>> invoiceItemsFromDockets = List.empty(growable: true);
for (var docket in dockets) {
invoiceItemsFromDockets.addAll(docket.toXeroLineItems(collectedFrom, conNumber));
}
if (invoiceGet.statusCode == 200) {
var responseData = jsonDecode(invoiceGet.body) as Map<String, dynamic>;
invoiceItemsFromDockets.sort(_compareLineItemDates);
if (responseData['Invoices'].length == 0) {
Logger().v('No invoice found, creating new monthly collation invoice for $contactName');
invoiceData = {
'Type': 'ACCREC',
'Contact': {
'Name': contactName,
},
'LineItems': invoiceItemsFromDockets
};
await submitCollatedInvoice(invoiceData, true);
} else {
invoiceData = responseData['Invoices'][0];
var invoiceItems = List.castFrom<dynamic, Map<String, dynamic>>(invoiceData['LineItems']);
for (var lineEntry in invoiceItems) {
// Reset the line amounts for recalcuating - sometimes rounding will cause an issue and a mismatch, causing the invoice to fail
lineEntry.remove('LineAmount');
}
invoiceItems.insertAll(0, invoiceItemsFromDockets);
invoiceItems.sort(_compareLineItemDates);
invoiceData['LineItems'] = invoiceItems;
await submitCollatedInvoice(invoiceData, false);
}
} else {
Logger().e(invoiceGet.body);
return Future.error('Error updating/creating monthly Customer invoice: ${invoiceGet.body}');
}
}
Future submitCollatedInvoice(Map<String, dynamic> invoiceData, bool newInvoice) async {
await _checkToken();
if (newInvoice) {
var invoicePut = await http.put(
Uri.parse('https://api.xero.com/api.xro/2.0/invoices'),
headers: restHeader,
body: jsonEncode(invoiceData),
);
if (invoicePut.statusCode != 200) {
return Future.error('Failed to create a new collation invoice: ${invoicePut.body}');
} else {
// TODO: POST to invoice notes to show what dockets were added
}
} else {
var invoicePost = await http.post(Uri.parse('https://api.xero.com/api.xro/2.0/invoices'), headers: restHeader, body: jsonEncode(invoiceData));
if (invoicePost.statusCode != 200) {
return Future.error('Failed to update collated invoice: ${invoicePost.body}');
} else {
// TODO: POST to invoice notes to show what dockets were added
}
}
}
/// Sort by DeliveryDate and then CollectedDate if they exist in the line item description
int _compareLineItemDates(Map<String, dynamic> b, a) {
var aDesc = a['Description'];
var bDesc = b['Description'];
var aDeliveryString = _lineItemValueFromDesc(aDesc, 'Date Delivered:');
var aDelDate = formatStringToDateTime('dd/MM/yyyy', aDeliveryString);
var bDeliveryString = _lineItemValueFromDesc(bDesc, 'Date Delivered:');
var bDelDate = formatStringToDateTime('dd/MM/yyyy', bDeliveryString);
var aCollectedString = _lineItemValueFromDesc(aDesc, 'Date Collected:');
var aColDate = formatStringToDateTime('dd/MM/yyyy', aCollectedString);
var bCollectedString = _lineItemValueFromDesc(bDesc, 'Date Collected:');
var bColDate = formatStringToDateTime('dd/MM/yyyy', bCollectedString);
if (aDelDate == null && bDelDate == null) {
if (aColDate == null && bColDate == null) {
return 0;
} else if (aColDate == null) {
return -1;
} else {
return 1;
}
} else if (aDelDate == null) {
return -1;
} else if (bDelDate == null) {
return 1;
}
int deliveryDateComparison = aDelDate.compareTo(bDelDate);
if (deliveryDateComparison != 0) {
return deliveryDateComparison;
} else if (aColDate == null && bColDate == null) {
return 0;
} else if (aColDate == null) {
return -1;
} else if (bColDate == null) {
return 1;
} else {
return aColDate.compareTo(bColDate);
}
}
String? _lineItemValueFromDesc(String desc, String searchFor) {
var lines = desc.split('n');
var deliveryDateLine = lines.firstWhereOrNull((element) => element.contains(searchFor));
if (deliveryDateLine != null) {
var split = deliveryDateLine.split(': ');
if (split.length > 1 && split.last.trim().isNotEmpty) {
var delDateLine = deliveryDateLine.split(': ').last;
return delDateLine;
}
}
return null;
}
2
Answers
Spoke with Xero - there is no guarantee to the order, but the way to ensure the order you want is to delete the
LineItemID
key/value from each entry inLineItems
before submitting, otherwise Xero will try to place the entries back in where they were previously based on their existingLineItemID
s. This may cause issues with pre-calculated values inside yourLineItem
entries though, due to rounding errors, so check to clear those for recalculation as well.Also turns out you can ask Xero code related questions, even though they state you cannot, it just has to be about their API, and not your code using their API
json doesn’t guarantee key order.
https://datatracker.ietf.org/doc/html/rfc7159#section-1