Introduction Part of many Notes development projects, is a customer requirement that documents be numbered sequentially. The main reasons for such a requirement are:
- Legal (some government requires it for a certain type of document)
- Aesthetic (because it seems tidier)
- Sorting (so the documents are in date order when sorted by identifier)
- Ease of reference (to have a short unique code that can be read over the phone)
- Audit (because auditors like to be able to spot a gap in the numbering sequence and know something went wrong there)
- Synchronize with serially numbered records in another system.
Clearly, these are not all equally compelling reasons from a business need standpoint, and they don't all require the identifiers to be sequential. This article discusses how to evaluate requirements for serial numbering, where you need to compromise to implement it, how to implement it, and various alternatives.
Assigning sequential identifiers is a challenge in Lotus Notes because of Notes' major strengths – its abilities to support high availability through clustering, efficient use of limited bandwidth by creating replicas in scattered locations that are linked by slower connections, and offline access by means of local replicas. There's no built-in function for sequential number assignment precisely because there are so many common situations in Notes deployments where this could not work. If a user is working in an offline replica, there's no way, even in theory, to make sure the numbers they assign to new documents are in sequence with numbers being assigned elsewhere.
The key points covered here:
- How to explore the constraints of document numbering requirements with a client.
- Techniques for assigning unique non-sequential identifiers.
- Techniques for assigning sequential numeric identifiers.
- DO NOT use @DbColumn to read a list of the numbers used so far so you can increment the last one. This is a performance killer.
- Other pitfalls to avoid.
Disclaimer: All code in this document should be treated as an example only; IBM doesn't guarantee that it works or is right for your situation. You will need to adapt and test it for your application. If you find any errors, please be courteous -- come back and correct them!
Background
Any system that assigns sequential numbers on demand, can only work if there's a single, centralized provider of numbers (a "number server") and continuously available network access to the number server from the point where the numbers are actually assigned. If the network between point A and point B is down, then there's no way for the system to know that users at point A are creating documents with the same identifiers as users at point B. Even if all users are on a single cluster of Domino servers, it takes a little time for an ID assignment done by one server, to replicate to all the other servers in the cluster. During that time, another server might assign the same ID. If you need 100% assured uniqueness, then you have to have only a single server assigning the numbers at any given time.
Which server is the "number server" might change over time, for instance in a high-availability environment where you failover the number assignment responsibility. However, the handover period is tricky, as there's potential for a situation where the old number server assigned a number but failed to get that information out to the backup servers before it crashed, so that the failover server will begin by assigning the same number.
So, unless you're prepared to do a lot of work to make 100% certain that numbers are assigned in exact consecutive sequence, then you must compromise your requirements somehow -- you can compromise on:
- Certainty that the number is unique, or
- Availability, or
- Certainty that the numbers are in sequence, or
- When the number is assigned (for instance, you could have a server scheduled agent to assign document numbers, instead of assigning them immediately when the document is composed/saved), or
- Forget about sequence and just assure they are unique.
Negotiating the Requirement What's the
business need behind the identifier? What are the minimum requirements to satisfy the business need? What's the earliest point at which the identifier needs to be assigned?
As you talk with customers about this, the terms "single point of failure," "high availability," and "mobility," repeated often, may help you make your point.
The main difficulty arises when assigning identifiers when a document is being composed. If the assignment can be deferred until it can be done by a central process, that's easy to implement, since a server agent can just look for the last used number and increment it. You just have to choose a time to assign the number when it's unlikely a user will be editing it (to avoid replication/save conflicts).
The main reasons that customers ask for sequential numbering (and your possible responses):
- There is a legal requirement to have consecutive numbers.
There's not much point in arguing with a government about what's sensible. But the document can wait to have a "legal identifier" assigned until it becomes necessary, so that it can be done centrally by a server agent. In the interim, if needed, it can be referenced by another identifier assigned at compose time, which is unique but not sequential. - It satisfies the customer's sense of order to have a sequential value assigned when the document is created.
Is tidiness important enough that you have to insist that nobody can work off-line? Is it critical enough to justify a single point of failure of the system, in case the number server goes down? Does it justify taking an extra second for each document created at a remote location with a slower connection? - They need it for the documents to sort properly.
No, they don't. Documents can easily be sorted by date/time created, or you can assign an identifier that, while not a sequential number, is still "larger" for documents created later. - They need a reference number for call center staff to find the document quickly, which can be easily read over the phone.
There's some reason to have a numeric identifier here, because they are easier to read over the phone (even if they might need to be a couple of characters longer than an alphanumeric identifier). However, it doesn't need to be sequential to be used for this purpose, merely unique. - The auditors like a consecutive numbering sequence so they can spot when something's been deleted.
Not only is it laborious and error-prone to scan a list looking for missing numbers, but knowing something's been deleted doesn't let you tell what was deleted, who deleted it, or let you retrieve it if you need it back. How about, instead, we show you a list of what was deleted and by whom, and let you restore things if you choose? Do you still need consecutive numbers then? - They need to synchronize the information with a relational table that uses sequential IDs.
No problem. You can arrange for the ID to be selected by a server process later on -- perhaps by the relational database -- when the document is first synchronized, as opposed to when it's composed.
Here are some questions you might ask to pin down the requirement.
- What purposes does the identifier serve?
- What's the first time a document's identifier is used for any task? For what task?
- When does the identifier need to be assigned: when the document is composed, when it is saved, or will it be sufficient to assign it at some later time (for instance, using a server agent).
- If identifiers have to be consecutive,
and they have to be assigned when the document is composed (before it's saved), what if someone composes a document and abandons it without saving, creating a gap? What if they don't save until three days later, and meanwhile documents with higher numbers are created? Is that a problem?
- What specific problems does a gap in the numbering sequence cause?
- What specific problems does it cause if the sequence is out of order relative to documents' creation date/times?
- Is it permissible to have a prefix or suffix of extra letters or numbers to help guarantee uniqueness? (It would be a suffix, if the ID sort order must match creation date/time sort).
- Will users always have network access to one particular server when they are creating numbered documents (so that you can always ask that server for the next unique number)? Is it acceptable that if the "number server" is down, no new documents can be created? (for long-term or planned outages, the number server can be changed)
- Suppose there's a small chance that a duplicate identifier is assigned. How big a problem is this? We could come back later and automatically fix the duplicate identifiers by adding a suffix A or B, for instance.
Having established what the minimal requirements are, you can consider which of the following approaches will meet the need.
@Unique
The macro function @Unique, used in a "computed when composed" field, generates an identifier such as the following: AGUD-7CAS93. The part before the dash (AGUD) is derived from the user's name. The part after the dash is based on the current date and time, down to 2 or 3 seconds accuracy. The Notes client (or Domino server, for a web application) keeps track of the last value generated, so even if the same user uses @Unique many times in quick succession, they get a different value each time.
The date-based part of the identifier is a number in base-32 notation, using the alphabet (except for I and O) and numerals (except for 1 and 0) as digits. So, if you need an identifier whose alphabetical sort is the same as by the date/time the identifier was assigned, you can create it with the following formula:
_tmp := @Unique;
@Right(_tmp; "-") + "-" + @Left(_tmp; "-")
An example return value is 7CAS93-AGUD.
Whether you use this formula or just @Unique, there's a small chance of duplicate identifiers if two users with similar names create documents within a couple of seconds of each other. In practice, unless you have a great many users creating a documents, the risk is small.
The advantages of this approach are:
- It's extremely easy to implement.
- Performance is excellent.
- The sort order is the same as date-created order, if you wish.
- The ID can be assigned at compose time (though doing so may make that ID sort in a different order than "date/time created," if the user waits long before saving).
- It doesn't need a "number server," and can be used offline.
- The identifier is reasonably short. When searching for a document via document "quick search" in a view, the user need type fewer characters to locate the document, as compared to a numeric identifier with leading zeroes. Usually, the date/time part will be unique, so the user can find a document by searching for only that part of the ID.
- It doesn't require the user to be "set up" in any way.
Disadvantages:
- A duplicate identifier is possible, though pretty unlikely unless this is a high-volume application.
- Because of the similar sounds of some letters' names (B and D for instance), it may be more difficult to use identifiers containing letters if the identifier is read over the phone.
Unique user "initials" Using @Unique, since multiple users might have the same prefix, there's a small chance of a duplicate id being assigned.
One way around this is to assign each user a unique short string that identifies their documen
ts, and use that in place of the prefix generated by @Unique. You could store these in a special document and view, so that they can be looked up as needed with a formula such as:
_initials := @DbLookup(""; ""; "(UserPrefixes)"; @V3Username; 2; [FailSilent])[1];
_tmp := @Unique;
@Right(_tmp; "-") + "-" + @If(_initials = ""; @Left(_tmp; "-"); _initials)
You can extend this; for instance, you might add LotusScript code on document compose to check whether the user has "initials" already defined, and prompt them to choose a value which the code then stores in a lookup document (after checking that it's unique).
Advantages:
- Allows offline document creation.
- Duplicate identifiers are extremely unlikely or (with advance setup of users) impossible, even for mobile users.
- It's pretty easy to implement, and the design elements can be reused in other applications.
- The identifier will sort in date-created order, if you wish.
- The identifier can be assigned on compose (with the same proviso about sorting).
Disadvantages:
- Since it requires one @DbLookup, this isn't as fast as just using @Unique.
- It's not quite as trouble-free since we must maintain the user "initials" documents and worry about what happens if users choose duplicate initials in different replicas.
A similar system could be created that provides all-digit identifiers for less ambiguous reading over the phone, using the user's unique prefix and a number calculated based on the current date/time. The following formula converts the date/time part of @Unique to a decimal number (9 digits long):
_initials := @DbLookup(""; ""; "(UserPrefixes)"; @V3UserName; 2; [FailSilent])[1];
_tmp := @Unique;
_suffix := @If(@IsError(_initials); @Left(_tmp; "-"); _initials = ""; @Left(_tmp; "-"); _initials);
_from := "2":"3":"4":"5":"6":"7":"8":"9":"A":"B":"C":"D":"E":"F":"G":"H":"J":"K":"L":"M":"N":"P":"Q":"R":"S":"T":"U":"V":"W":"X":"Y":"Z";
_i := -1;
_to := @Transform(_from; "x"; @Text(_i := _i + 1) + ",");
_digits := @ToNumber(@Explode(@ReplaceSubstring(@Right(_tmp; "-");_from; _to); ","));
_cur := _digits[1];
@For(_k := 2; _k <= @Elements(_digits); _k := _k + 1; _cur := _cur * 32 + _digits[_k]);
@Text(_cur) + "-" + _suffix
This uses the same "initials" scheme as above, and you can decide whether the user suffix is letters or digits. The part of the ID before the dash is very likely to be unique, so the suffix isn't generally needed to locate the right document anyway. There's usually no need to read the suffix over the phone, since even if there's a duplicate for the first part of the ID, the user can generally figure out which is the correct record based on on other information available to them. So it can contain letters without causing any difficulty.
Number Assignment Server Agent
Suppose you have to assign a sequential numeric ID, but you don't have to do it immediately when the document is composed. How would you implement this as a server agent?
First, consider when users are likely to be editing documents, to avoid causing save/replication conflicts by your agent assigning an ID while someone is editing the document. This is a consideration for any agent that modifies documents, not just for ID assignment. If users have local replicas, they might be editing documents one day and fail to replicate until the next day, so just running your agent late at night when you don't expect anyone to be working, isn't a guarantee that you won't cause a conflict.
If you have a formal workflow, you might consider making number assignment a stage in your workflow -- handled automatically, of course -- and not allow anyone to edit the document while it is in that workflow state. So you could write an agent to search for documents at that workflow state, assign them the next consecutive number, and at the same time advance the workflow to the next sta
te. Such an agent could run on "new and modified" documents, so there's no appreciable delay and the number can be assigned during working hours.
It's best to use LotusScript or Java for such an agent. It's possible to write this using a formula, but this is not very efficient since you would have to use @DbColumn (see note about this below). Assuming you have a view that's sorted by document number, you might code it as shown in listing 1. This code is just an example; your form names, field names, names of workflow status, and so on, may all be different.
If using a sorted view to find the last-assigned ID, be aware of the distinction between number and text sorting. If your identifier is stored as a string, it will be sorted alphabetically. With numbers, 999 < 1000 < 1001, but with strings, "1000" < "1001" < "999". If your identifiers are strings, either have enough leading zeroes to assure they're all the same length, or have a view column formula such as @ToNumber(DocID) so that you can sort them numerically.
Set this agent is set to run on just one server, and if you have to change which server that is, make sure the replicas are up to date before you do so.
Advantages of this approach:
- Reasonably easy.
- Very reliable.
- Allows off-line authoring.
- Zero performance impact for users.
Disadvantages:
- The number isn't available immediately on compose, so it's no good for call center applications, if you need an ID to read to a customer over the phone. In such cases, you might use a dual-ID system, with one ID created at compose time that's unique but not sequential, and a second, sequential ID assigned later on.
NOTE: It's possible to use similar code to assign numbers on demand, but this presents challenges:
- If there are multiple replicas (even if they're clustered) there's some delay before a server knows about a document created on another server, so there's a high likelihood of duplicates.
- If you don't save the document immediately, there's no way for other users to know that the number has already been assigned.
- Even if there's just one replica, and you save immediately, there's a short window of vulnerability -- between the time you read the last-used number and when your save is completed -- during which another user may read the last-used number and get assigned the same number you are about to use. This might be on the order of a second or so, depending on various factors.
Number Assignment On Demand If you determine that you need a sequential number assigned when the document is composed or first saved, how can you provide this functionality?
There are third-party products that support this by providing a stand-alone number server. But you can very easily use a Notes server as your number server without the added overhead of running an agent or web service for every number request.
One way to do this, is to have a single NSF on a server holding documents that other applications can update to record their "last used" number. Each application would "own" one document that contains the last-used number for that application. The calling application uses code such as that in Listing 2, below. This code takes advantage of the ability of the Domino server to abort a save if a document has been modified since it was read, making it impossible for the same number to be assigned to two users who issue requests at the same time.
The caller of this function can use the return value as it likes; it might just store the number in a field, or it might format the numeric return value into a string with leading zeroes or other decorations. Since the last-used number is tracked separately from the value stored in the document, it doesn't matter how the number is manipulated by the caller.
For configurability, the server and filepath of the numbering database and UNID of the document, can be stored in a profile document
in the application that contains the documents being numbered. Note we are using a UNID here because of the efficiency of locating a document by UNID, but if you wanted to use a key string in a view lookup to find your numbering document, that would also work.
Advantages of this approach:
- It gets you a sequential number containing only digits.
- The numbers are in order by creation date.
- The code can be reused.
- It's possible to create not only multiple replicas, but multiple applications that share the same pool of sequential numbers, if you wish.
Disadvantages:
- Offline document creation is not an option.
- The Domino numbering server is a single point of failure for the system.
- If some users have slower connections to this server, they may notice a delay when assigning the number, compared to accessing a database on a nearby server.
- It's hard to absolutely guarantee that an assigned number gets saved into the application, leading to possible gaps in the numbering.
- Implementation to minimize this possibility is a more complex than would at first appear, as discussed below.
Note: The references contain another technique for doing essentially the same thing.
How to apply on-demand numbering on your form
You must decide when it's appropriate to assign the ID. If you assign it when the user has just composed a document (in the Postopen form event, for instance), then there's a chance that they won't save the document, which would leave a gap in your numbering sequence. Even if you refuse to let them exit the document without saving, there's such a thing as a crash or power-off.
Or, they might not save it for a long time, and meanwhile other documents with larger numbers are saved, which would appear out of sequence.
For your application, you might not care about either of these things. But if they don't matter, ask yourself why you're going to all this trouble to assign sequential numbers that don't really need to be sequential, and consider using one of the other techniques described above.
The obvious alternative to assigning on compose, is to assign the number when the document is first saved. So you might be tempted to put the code in the Querysave event. However, during Querysave there's no guarantee that the save will succeed. It might fail because of a validation failure, or because the application server isn't available. In that case, once again, you can be left with a gap if the document never gets saved, or an apparent out of sequence because the user delays trying to save again.
Note: the number server being available is no guarantee that their application server is, too. They aren't necessarily the same server.
We would like to assign the number as the document is first being saved, and only if the save succeeds. This is tricky. There's no way to tell whether the save will succeed except by trying it. So you would have to save the document twice; the first time to show that you can do it, then again after assigning the number.
Note: this will succeed only if the user has access to edit the document once it is saved, so they must either be an Editor in the database ACL, or they must be an Author with an Authors field in the document that allows them to edit it.
There are basically two ways to implement this. You can either force users to use an action button to save if there is not a number assigned, or you can do it all in a Postsave form event.
Assign Number Using Action Button
The idea here is that if there is no number assigned, the user must use an action button to save the document. The action contains code that saves once (to prove that you can), then assigns the number, then saves again. You would have to add a check in Querysave to assure that the document is only saved with this action if
the number hasn't been assigned yet.
Listing 3 shows example code for doing this -- it assumes you already have included the code from listing 2 in your Globals section, and makes further assumptions about your field names. This is not fully tested -- caveat emptor.
Assign Number in Postsave
Alternately, you could make the Postsave event fetch the number and resave the document, but this only works if the second save is a back-end save followed by your closing the document. You can't use a front-end save from Postopen because that would be a recursive event invocation. You can't do a back-end save and leave the document open, because if the user makes further changes and saves again, they would be creating a save conflict with their own back-end save. It is possible for the Postopen event to close and then immediately reopen the document, which should give the same effect as if it remained open for editing. Listing 4 gives an example of this approach.
This is less code than the approach that uses an action button, and is simpler overall. It has one drawback, though: it closes and reopens the document. There are cases where you don't want to do this; for instance, if you have a LotusScript action on the form that saves and then does anything else on the form (which has the potential to cause a crash because of code operating in a context that no longer exists), or if the Queryopen or Postopen code is complex, or does something you don't want it to do at that point.
You may note a slight difference in the Queryclose event in this example, compared to listing 3 -- if the user insists, it will let them close the document without saving the second time. This is not a requirement for this technique -- it's an alternative you may prefer, based on the fact that the user might not be able to complete the save themselves. In that case you will need fallback techniques to assign the number (for instance, a nightly agent that would look for documents with no numbers assigned and try to match them to gaps in the sequence, or a method to manually enter a number later on).
Considerations on reliability of this technique
If you implement number assignment on demand as described above, there's a small chance that the number assignment will succeed but the second save will fail. This would present a problem. The number server thinks the number has been used, but the document using it hasn't been saved, and there's a chance it might never be. This would result in a gap in your numbering sequence. We handle this in the sample code by advising the user of the problem and hoping they have the good sense to not turn off their computer without completing the save (and also using Queryclose to remind them if they do forget and try to exit anyway).
If the user has no access to edit the document, it's not possible for them to correct the problem on their own. This is a problem in the design of the application.
In all other cases, the chance of failure on the second save, coming just a second or so after the first save, is small, and it should be possible for them to do something to correct the problem -- re-establish their VPN, wait for the server to come back up, etcetera. As a last resort, if they're unable to deal with it themselves, they could note down the number they did get assigned, and they or a specially authorized user could find the document and manually assign the right ID, using a control that's hidden except in documents that have already been saved but have a blank ID.
High Availability Number Service
For serious, high-end applications where high availability is a requirement, it's probably possible to buy or rent software that runs on multiple servers. There are several features such an application should have; besides the multiple servers, there would have to be some reliable mirrored storage, and if the multiple servers are active at the same
time, a mechanism for "locking" the last-used-number storage. If you have a relational database with these characteristics, it should be possible to write a stored procedure which can be invoked by a web service running on clustered Domino servers, for instance. This sort of thing costs money – either in developer time, or paying for a product to do it.
If you use such a tool, you still have the same problems discussed in the previous section, about how to guarantee that the assigned number gets saved. Some number servers might use a two-phase mechanism, where after assigning the number they wait for you to tell them that the number was used, before responding to other requests.
Mistakes to Avoid
@DbColumn
It's common for numbers to be assigned by a computed field formula. We often see formulas such as the following:
REM {This is an example of what NOT to do};
tmp := @DbColumn("":"NoCache"; ""; "RequestsByNumber"; 1);
nextNumber := @If(tmp = ""; 1; @ToNumber(@Subset(tmp; -1)) + 1);
@Right("000000" + @Text(nextNumber); 7)
This reads all the document IDs already in use, from a view of all documents sorted by ID. It takes the last one (or first, depending how the view is sorted), and adds one to get the next number.
This is easy to implement, but it has serious drawbacks:
- The @DbColumn function, which works nicely when you have a few dozen test documents on a local server, is very slow when returning data from thousands of documents on a server on another continent.
- If documents are created in more than one replica, duplicate IDs will be assigned, since they each are only looking at the documents already in their replica.
- The above is still a problem if you change the formula to always refer to a specific replica (on server A) instead of the current replica. When someone composes document #17 on server B, until the next replication server A doesn't know about that document to prevent another #17 being created.
- Even if documents are only composed in a single replica, there's a chance of duplication when documents are being saved at the same time from different workstations, because of the short delay between reading the values from the view, and the document save being completed.
- If the number is assigned when the document is composed, there's a large window of vulnerability for duplicate numbers, until the user chooses to save the document.
Profile Documents Because they are better performing than the alternatives of reading regular documents and views, and because they're so easy to use, it's tempting to use a profile document to store the "last used" number.
However, profile documents are efficient because they're cached on each user's workstation (and in a Domino web application, different server threads may each have their own cache). Since user A may not immediately see the change made by user B, they will get duplicate identifiers. Profile documents appear to work for numbering, when you test them with only one person authoring documents. That's because the user's cache is always up to date with their own changes -- but not with changes made by others.
Profile documents are appropriate for application configuration -- not for any kind of data that changes frequently.
References
Generating sequential numbers in replicated applications by Jonathan Coombs, Lotus developerWorks technical library, 4 Feb 2002 presents alternate code that implements a similar approach to the "GetNextNumber" function described here.
Sample Code
Listing 1: server agent to assign numbers as a stage in a workflow (example)
Sub Initialize ' document selection is something along the lines of: ' ([Form] = "Invoice") and ([Status] = "NumberAssign") Dim session As New NotesSession Dim coll As NotesDocumentCollection Dim db As NotesDatabase Dim docCur As NotesDocument Dim vuByID As NotesView Dim lastUsed As Variant Dim nextNum As Long Set db = session.CurrentDatabase Set coll = db.UnprocessedDocuments If coll.Count Then ' there are some documents to do. Set vuById = db.GetView("DocsByIDAscending") lastUsed = vuById.GetLastDocument().ColumnValues(0) If Isarray(lastUsed) Then lastUsed = lastUsed(0) nextNum = Clng(lastUsed) + 1 Set docCur = coll.GetFirstDocument Do Until docCur Is Nothing docCur.ReplaceItemValue "Status", "NextStatusAfterAssigningNumber" docCur.ReplaceItemValue "IDNum", nextNum ' or, e.g. Format(nextNum, "0000000") If docCur.Save(False, False, True) Then ' save succeeded -- mark as processed. Call session.UpdateProcessedDoc(docCur) End If nextNum = nextNum + 1 Set docCur = coll.GetNextDocument(docCur) Loop End If End Sub |
Listing 2: generic GetNextNumber function for assigning number on demand Function GetNextNumber(db As NotesDatabase, Byval strUNID$, Byval strItemName$) As Long ' db is database containing a doc where the "last used" number is stored. ' strUNID is the Universal ID of the document. ' strItemName is name of item in that doc containing last used #. Const MAXTRIES = 5 ' if we get this many save conflicts something's wrong. Dim docCtr As NotesDocument Dim tries% Dim valCtr For tries = 1 To MAXTRIES Set docCtr = db.GetDocumentByUNID(strUNID) valCtr = docCtr.GetItemValue(strItemName) If Not Isnumeric(valctr(0)) Then ' first document is number 1 GetNextNumber = 1 Else GetNextNumber = 1& + Clng(valctr(0)) End If docCtr.ReplaceItemValue strItemName, GetNextNumber If docCtr.Save(False, False, True) Then ' save worked! Exit Function Elseif tries < MAXTRIES Then ' save conflict -- try again tries = tries + 1 Delete docCtr ' clear it out of the cache. Else Error 19891, "Retry count exceeded in GetNextNumber" End If Next End Function Function GetDocID(dbThis As NotesDatabase) As Long ' This function is an example of how the application might call GetNextNumber, ' supplying it information read from a profile document. Dim dbNumberServ As NotesDatabase Dim docProfile As NotesDocument Dim myNumber As Long Set docProfile = dbThis.GetProfileDocument("MasterProfile") Set dbNumberServ = New NotesDatabase(docProfile.GetItemValue("NumDBServer")(0), _ docProfile.GetItemValue("NumDBPath")(0)) GetDocID = GetNextNumber(dbNumberServ, docProfile.GetItemValue("NumDBUNID")(0), "NextNumber") End Function |
Listing 3: Example Form code to use GetNextNumber/GetDocID and prevent gaps in the sequence ' (Globals) Option Declare ' always use Option Declare %INCLUDE "lsconst.lss" Dim GokToSave As Boolean Dim GpreventExit As Boolean ' (Form events) Sub Querysave(Source As Notesuidocument, Continue As Variant) If Not GokToSave Then ' prevent save without assigning an ID first, unless this is a test save done by a special action button. Dim doc As NotesDocument Set doc = Source.Document If doc.GetItemValue("DocID")(0) "" Then Msgbox "Please use the Save action to save the document (because the ID number has not yet been assigned)", _ MB_OK+MB_ICONSTOP, "Save Invoice" Continue = False End If End If End Sub Sub Postsave(Source As Notesuidocument) ' Save succeeded. If preventing close window because an ID was assigned ' but not saved, close window is now allowed. GpreventExit = False End Sub Sub Queryclose(Source As Notesuidocument, Continue As Variant) If GpreventExit Then Msgbox {A document ID has been assigned, but you were unable to save the document. You MUST save the document before exiting to prevent a gap in the numbering sequence.}, _ MB_ICONSTOP, "Close Invoice: Blocked" Continue = False End If End Sub ' "Save" action code Sub Click(Source As Button) GokToSave = True Dim wksp As New NotesUIWorkspace &nb sp; Dim uidoc A
s NotesUIDocument Dim doc As NotesDocument Set uidoc = wksp.CurrentDocument On Error Goto oops uidoc.Save Set doc = uidoc.Document If doc.GetItemValue("DocID")(0) = "" Then On Error Goto cantGetNumber doc.ReplaceItemValue "DocID", Format(GetDocID(doc.ParentDatabase), "00000000") On Error Goto secondSaveFailed uidoc.Save End If Exit Sub cantGetNumber: ' we saved the first time, but couldn't assign a number. Msgbox {Attempt to assign ID number failed because: Error } & Err & {: } & Error & { Please correct the problem and try again.}, MB_ICONSTOP, {Save Invoice} oops: GokToSave = False Exit Sub secondSaveFailed: Msgbox {An invoice number was assigned, but you were unable to save the document with the new invoice number because: Error } & Err & {: } & Error & { You may not close this window until you save the document, or there will be a gap in the numbering sequence.}, _ MB_ICONEXCLAMATION, "Save Failed" GpreventExit = True ' remember to prevent illegal window closure. ' GokToSave is still true from above. Since the number is already assigned, we have no ' objection to saving with Ctrl+S. Exit Sub End Sub |
Listing 4: Example form code to assign number in Postsave form event ' (Globals) Option Declare ' always use Option Declare %INCLUDE "lsconst.lss" Dim GpreventExit As Boolean ' (Form events) Sub Queryclose(Source As Notesuidocument, Continue As Variant) If GpreventExit Then Dim result% result = Msgbox({A document ID has been assigned, but you were unable to save the document. You MUST save the document before exiting to prevent a gap in the numbering sequence. Exit anyway?}, _ MB_ICONEXCLAMATION+MB_YESNO+MB_DE FBUTTON2, "Close Invoice: Warning!") If Not result Then Continue = False End If End Sub Sub Postsave(Source As Notesuidocument) ' Save succeeded. But do we have an ID number yet? GpreventExit =False Dim doc As NotesDocument Set doc = Source.Document If doc.GetItemValue("DocID")(0) = "" Then ' No ID number -- try to get one On Error Goto NumberFail doc.ReplaceItemValue "DocID", getDocID(doc.ParentDatabase) ' ID number fetch succeeded. Try to save doc again with the new number. On Error Goto ResaveFail If doc.Save(False, False, True) Then Source.Close True Dim wksp As New NotesUIWorkspace Call wksp.EditDocument(True, doc) Delete Source Else ResaveFail: GpreventExit = True ' remember to prevent illegal window closure. Msgbox {An invoice number was assigned, but you were unable to save the document afterwards because:
Error } & Err & {: } & Error & { You may not close this window without saving, or there will be a gap in the numbering sequence.}, MB_ICONEXCLAMATION, {Resave failed} End If End If Exit Sub NumberFail: Msgbox {Save succeeded, but unable to assign invoice number because: Error } & Err & {: } & Error & { Please correct the problem and try again.}, MB_ICONEXCLAMATION, {Number assignment fail} Exit Sub End Sub |
size="5">
Employ Document Locking to assure number uniqueness Added by Marko Bonaci, 02.04.2010.
I noticed that many inexperienced Lotus developers develop unreliable document numbering solutions, usually employing view counting method.
To be completely honest, the first thing that came to my mind 6 years ago was exactly that.
Why "counting view documents" approach is not reliable way to go?
Let's analyze how that works:
1 - new document receives save request from the user (QS) and calls the function that gets next number
2 - the function gets a database and a view (sorted descending by doc number)
3 - the function sets the reference to the top view document
4 - the function reads the value from the document field (or counts the number of docs in a view)
5 - the function returns last number incremented by 1
6 - new document receives value from the function and stores it in a field
7 - new document is saved
Now, what if there are multiple new documents being saved on, lets say 3 workstations, at the same time.