skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 in LineItems before submitting, otherwise Xero will try to place the entries back in where they were previously based on their existing LineItemIDs. This may cause issues with pre-calculated values inside your LineItem 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


  2. json doesn’t guarantee key order.
    https://datatracker.ietf.org/doc/html/rfc7159#section-1

    An object is an unordered collection of zero or more name/value
    pairs, where a name is a string and a value is a string, number,
    boolean, null, object, or array.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search