SuperOffice CRM Professionals

Experienced experts in SuperOffice CRM - those primarily responsible for the setup, configuration, maintenance of SuperOffice in an organization, as well as those responsible for the implementation of applications or integrations, or provide consultancy services as to how an integration should be implemented using current industry standards.


Technical blog posts

New Failures feature in Developer Portal

Hello, SuperOffice developers! We are excited to announce a new feature in the SuperOffice Developer Portal that will help you troubleshoot and fix any issues with your online integrations and applications. The feature is called **Failures** and it is documented at . The Failures feature allows you to view and analyze general application exceptions, and drill down into exceptions from Inbound requests. The Failed Outbound Requests tab is reserved for requests sent from SuperOffice to your applications, such as Quote/ERPSync calls and webhooks, but this part of the feature is still a work in progress (awaiting on more logging capabilities). To access the Failures feature, you need to sign in to the Developer Portal and select your app from the list of your apps. Then, click on the Failures tab on the left side of the app page. You will see a table with all the failed requests for your app in the last 30 minutes. You can click on any row to see more details about the failure. You can also use the filters on the top of the table to select the correct environment , and time range to narrow down the results. The Failures feature is available for all apps that use SuperOffice Online APIs.We hope that this feature will help you develop better apps for SuperOffice CRM Online and provide a better experience for your customers. If you have any feedback or questions about this feature, please let us know here in the forums or contact Thank you for using SuperOffice Developer Portal!

Tony Yates
thumb_up1 mode_comment0 visibility9

TravelTransactionLog changes

The TravelTransactionLog ("TTL") table dates back to the deep past of SuperOffice, when the Internet was a nice idea and crappy/expensive hotel phone lines were the reality. Being able to take the database with you was a real power feature. Even better, you took only a subset that was relevant to you, saving expensive megabytes on your little harddisk. NetServer still notes changes in TTL, and it's still valuable as a record of changes being made - at least to tables that were part of the Travel system. Most of this logging is relatively cheap and worth keeping. However, there are several special events that complicate the code and have some performance overhead. These are the changes that would, in the old days, have influenced the filters used to define the subset of the database that users would take with them: changes to contact.associate_id (and a few obscure tables only used for the Area Management configuration in Travel). The other category is logging of per-field changes on Udef tables. Both of these can result in additional database traffic (because we need to know the previous value), and we can't write the actual change until that information has been secured. In the TTL table, these events have event ID's higher than 4864. The basic events for Insert (4352, 0x1100), Update (4608, 0x1200) and Delete (4864, 0x1300) will be kept; but we want to stop logging all other events. If you have no idea what I'm writing about here - fine! No problem!   But if this causes cold sweat and incipient panic then I need to know. Cheers, Marek  

Marek Vokáč
thumb_up2 mode_comment3 visibility49

The TicketSentry is getting teeth

There has been a Sentry mechanism in NetServer for requests (well, the table is called ticket ) for many years. Ticket rights are not calculated in the same way as for Sales entities: instead of an owner and group system and a data access matrix, tickets belong to categories ( ej_category ) and those categories have masters and members. Finally, users belong to roles that have specific functional rights, and those functional rights are used to grant types of access to tickets. As with sale or appointment, access rights calculated for the root table will also lead to specific rights for derived tables (think appointment -> text). In the case of ticket, the ej_message table is the most interesting derived case. Think of it as category replacing the owner/group distance concept in Sales; and the functional rights correspond to the different columns in the data access right matrix in admin, for Sales entities. This TicketSentry in NetServer has, up to now, only worked with the type of access required for so-called List of tickets. In Service that means you get to see tickets in grids, but you can't change them and you can't see the messages belonging to those tickets. The List/View/Edit levels in Service don't have a direct counterpart in Sales entities, but it actually maps pretty well to table rights. List => ticket - Read , ej_message - None View => ticket - Read , ej_message - Read Edit => ticket - Full , ej_message - Full This is the mapping we'll have from now on. But, until now it was " if you have List, you get FULL on everything; otherwise you get NONE on everything; and we don't care about View or Edit ".   That wasn't really good, and it will now change to the rights mapping as outlined above. The rights calculations themselves do not change (no refactoring there!), so the various rules for members/masters/non-service-users etc are exactly the same as before. But they will now be enforced by the database layer in NetServer, and they will appear properly in the FieldRight dictionaries on TicketEntity carriers. As an example, if you perform a query that takes its first field from the ej_message table, and you do not have the right to see the ej_message part of some record in the result set, then that record will disappear completely. Even if it is joined to things that you do have access to - the ej_message part of the record will decide the outcome, because it is the first field you refer to. In other words - GetTicketMessageEntity(123) will start returning null if you don't have access to that message (calculated via its ticket, and you relationship to that). Likewise, a dot-syntax search where the first field is ej_message. will be treated in the same way - either you have access to that row, or you never see it. If you are getting hold of ej_message fields in some other way - say, as a dotsyntax query that starts with ticket - then the ticket may be there (if you have access), but fields belonging to ej_message may be blank (if you don't have access to that message). I will just reiterate that the rules have not changed. But we are starting to enforce them, within NetServer, using the standard mechanisms and behaviours there. Hopefully you're not dependent on the old, laissez-faire behaviour... it's going away.

Marek Vokáč
thumb_up3 mode_comment0 visibility13

From the Curiosity Cabinet: .ForEach()

Sometimes you discover somthing in a corner of the code and think, "it must have seemed like a good idea at the time". As you delve into it, you get a sense of why; and at the same time, understand why it wasn't through a somewhat deeper insight. Get a coffee and bear with me while I explain. This time it's about LINQ in .NET and how it works. I expect everyone who uses C# is familiar with it, and even loves the power. Here in SuperOffice we've generally landed on the fluent-chain-of-methods syntax rather than the sql-like one. To me that's because it saves me from a mental gear-change. But in any case, the resulting code is the same and the same thing happens. One of the core features of LINQ is deferred execution . When you say var things = myCollection.Where(x => x.ShouldKeep).Select(x => x.TheThing); then nothing is actually done - yet. myCollection is not called upon to enumerate its members, no filtering and no projection happens. Only the chain is built. Then, when you say foreach(var item in things) { ... } ... and only then ... are the operations performed. This comes from the signatures of the methods - they take an IEnumerable , and return an IEnumerable . The important aspect here is that an IEnumerable is something that can be enumerated/iterated over; but it's not an enumerator/iterator in itself and it certainly doesn't mean the thing-to-be-enumerated is an already-existing collection. It's very powerful. But this deferral of execution needs to end, sooner or later: we actually want things to happen and we want to see the result. In the case of a foreach loop, we get handed the items one at a time. Another way is to say .ToList() , or .ToArray() , .ToDictionary() (one of my favourites); there are others. Their return types are not IEnumerable - instead they return some result; and their implementations contain a foreach loop that "pulls" on the incoming IEnumerable, thereby triggering the execution of the whole pipeline. With this background, consider the following code: public static IEnumerable ForEach(this IEnumerable enumeration, Action action) { if (enumeration == null) throw new ArgumentNullException("enumeration"); if (action == null) throw new ArgumentNullException("action"); foreach (var item in enumeration) { action(item); yield return item; } } With this in place, you might think it reasonable to write listOfAppointments.ForEach(m => Assert.IsFalse(String.IsNullOrEmpty(m.AssociateName))); But... no. The catch here is that the ForEach method does have a loop iterating over the incoming elements, sure... but in turn it yields elements through its return value. Therefore, it's not going to do anything unless the next method in the chain performs an iteration. It simply won't be called, because of the special way IEnumerable works and how the compiler generates code. In the example above, there is no next method; these Asserts were simply never executed. I'm pretty sure the author of that code didn't think that was going to (not?) happen. Years ago I got yelled at by a partner because of this, and because our .ForEach was in the global namespace and masking his own (better) implementation - which took him some time to figure out. I recently removed this curious and confusing construction. While LINQ is great for data set processing, I still think that updating elements in place or complex processing can just as well be in an explicit foreach loop. It makes it more clear that this stuff is actually happening to each element. The somewhat contrived "convert foreach to LINQ" example on Microsoft Learn is actually a case of a foreach loop that yields something - obviously a LINQ candidate, but very different from a loop that computes or stores things (what I'd call an action).  Of course this is a matter of opinion. But I think everyone agrees that our little curiosity, a .ForEach that doesn't actually execute by itself, is not what you expect or want. There. Done, removed, get on with Friday :-) Marek  

Marek Vokáč
thumb_up2 mode_comment0 visibility6

Upcoming CRMScript / Developer improvements

Dear community, We have done some improvements to CRMScript upcoming release that we hope will be useful for you. The changes can be split into two main areas: improvements to CRMScript and improvements to the developer UI. CRMScript improvements For a few years now, we have had support for serializing/deserializing arrays and structs to JSON data. However, the support had some "gaps" in support for all basic types, and also the usage was a bit cumbersome having to go through XMLNode or JSONBuilder . To solve the latter, we have now introduced two new functions on arrays and struct : fromJsonString() and toJsonString() . The functions will allow you to serialize/deserialize in one swift code line: struct Person { String firstname; String lastname; Date dob; }; Person p; p.fromJsonString('{"firstname": "Jon", "lastname": "Doe", "dob": "1984-10-26"}'); printLine(p.toJsonString()); The observant amongst you will perhaps also notice that in the code above, we are deserializing and serializing a member of type Date. This was not supported before, but now we actually support members of all basic types: Integer, String, Bool, Float, Date, Time, DateTime, Byte, TimeSpan. For the types not supported natively by JSON, we will require/produce the following formats: Date: converted to/from string with format "YYYY-MM-DD" DateTime: converted to/from string with format "YYYY-MM-DDTHH:MI:SS" Time: converted to/from string with format "HH:MI:SS" Byte: converted to/from number. TimeSpan: converted to/from number (seconds) Support for these additional types also work for the existing fromXMLNode() and toJson() methods. Also, please note that fromJsonString() on arrays will work just like fromXMLNode() , which means it will append elements to the array and not clear it. Arrays have a separate .clear() method that can be called if you want to empty it first. Furthermore, we have introduced two new methods on arrays: buildString() and sort() .  buildString(String separator) is adding some functionality that has always existed on Vector ; the possibility to create a string (with a separator) from an array. The function will work as expected on basic types. (Complex types (e.g. an array of HTTP instances) will just result in a string with "[complex]"). The most useful example is probably to get a comma-separated array of integers: Integer[] arr; for (Integer i = 0; i In addition to basic types, we also support buildString() on arrays of structs. The default way to serialize a struct instance is to return its contents as a JSON string. However, there is now a way to override this by adding a member function with the following signature: String toString() : struct Person { String firstname; String lastname; String toString() { return this.firstname + " " + this.lastname; } }; Person[] persons; persons.fromJsonString('[{"firstname": "Mark","lastname": "Wahlberg"}]'); persons.fromJsonString('[{"firstname": "Uma","lastname": "Thurman"}]'); persons.fromJsonString('[{"firstname": "Tom","lastname": "Cruise"}]'); persons.fromJsonString('[{"firstname": "Michael J.","lastname": "Fox"}]'); printLine(persons.buildString(",")); The other new function for arrays is sort() , which will obviously sort the contents of the array. Only one-dimensional arrays are supported, and only arrays of basic types or structs. For basic types, this works as you would expect: Integer[] arr; for (Integer i = 0; i In order to be able to sort an array of structs, the struct must implement a method with the following signature: Bool compare(SameStruct s) . This will allow you to sort arrays of structs using whatever comparison you'd like: struct Person { String firstname; String lastname; String toString() { return this.firstname + " " + this.lastname; } Bool compare(Person p) { return this.toString()   Developer UI improvements This release contains lots of small and medium improvements to the developer experience in Service. Here are some of the highlights: Debugger/Tracing view When debugging in real time, or when viewing a saved script trace, we have now added a dropdown to the UI with all the source locations for the current debug/trace session. For large scripts that are using #includes, this allows you to quickly switch between the different sources. In debug mode, you can use this to e.g. set a breakpoint in another file. In tracing mode, clicking in the gutter (where the red breakpoints are shown) will now instead fast-forward the trace to that location. This can be very useful when viewing large script traces: instead of having to use the slider to try to find the frame where some particular code is executed, you can rather click next to the code and the slider will move to the correct position. Another small but welcome improvement to this view is that the width of the sidebar (containing info, variables, etc) now will be remembered in your browser and reused on subsequent views. We have also cleaned up the variable view a bit, and added a "copy value to clipboard" icon for each variable. Working with CRMScripts, Custom screens and Extra tables We have also done some changes to the UI when working with CRMScripts. First of all, the search functionality for scripts will now show the chosen result using the correct "View script" screen instead of a dump of the table contents. Also, when viewing a script, the "Run script" link now properly uses the includeId of the script if it is configured. Using the includeId-link instead of the id-link is recommended to make customizations more robust when migrating between instances of SuperOffice. When editing a CRMScript, we have now changed the output frame to use a monospaced font. This makes for instance SearchEngine.executeTextTable() look much better. One issue that has been quite annoying for some time is the complex process for creating a trace for the scripts related to a Custom screen. This has now been simplified by adding a new tab to the "View screen" screen, listing all the scripts related to this screen. Just click a script to create a trace for it. Finally, a small improvement in the view for listing extra tables. We have now added links for searching a table or creating a new record in a table from this view, so you don't have to navigate through Requests > Extra tables, and then find the same table you just edited. Together with some other bug fixes, we sincerely hope our latest version of SuperOffice will be a welcome improvement to your developer experience!

Michel Krohn-Dale
thumb_up5 mode_comment3 visibility17