...
PROCEDURE: UpdateDetail
EMBED: %ProcedureRoutines 4000
!Calculate taxes, discounts, and total cost
!----------------------------------------------------------------------
CalcValues ROUTINE
IF DTL:TaxRate = 0 THEN
IF DTL:DiscountRate = 0 THEN
DTL:TotalCost = DTL:Price * DTL:QuantityOrdered
ELSE
LOC:RegTotalPrice = DTL:Price * DTL:QuantityOrdered
DTL:Discount = LOC:RegTotalPrice * DTL:DiscountRate / 100
DTL:TotalCost = LOC:RegTotalPrice - DTL:Discount
DTL:Savings = LOC:RegTotalPrice - DTL:TotalCost
END
ELSE
IF DTL:DiscountRate = 0 THEN
LOC:RegTotalPrice = DTL:Price * DTL:QuantityOrdered
DTL:TaxPaid = LOC:RegTotalPrice * DTL:TaxRate / 100
DTL:TotalCost = LOC:RegTotalPrice + DTL:TaxPaid
ELSE
LOC:RegTotalPrice = DTL:Price * DTL:QuantityOrdered
DTL:Discount = LOC:RegTotalPrice * DTL:DiscountRate / 100
LOC:DiscTotalPrice = LOC:RegTotalPrice - DTL:Discount
DTL:TaxPaid = LOC:DiscTotalPrice * DTL:TaxRate / 100
DTL:TotalCost = LOC:DiscTotalPrice + DTL:TaxPaid
DTL:Savings = LOC:RegTotalPrice - DTL:TotalCost
END
END
EMBED: %ProcedureRoutines 4000
!Update InvHist and Products files
!-----------------------------------------------------------------------
UpdateOtherFiles ROUTINE
PRO:ProductNumber = DTL:ProductNumber
Access:Products.TryFetch(PRO:KeyProductNumber)
CASE ThisWindow.Request
OF InsertRecord
IF DTL:BackOrdered = FALSE
PRO:QuantityInStock -= DTL:QuantityOrdered
IF Access:Products.Update() <> Level:Benign
STOP(ERROR())
END !end if
INV:Date = TODAY()
INV:ProductNumber = DTL:ProductNumber
INV:TransType = 'Sale'
INV:Quantity =- DTL:QuantityOrdered
INV:Cost = PRO:Cost
INV:Notes = 'New purchase'
IF Access:InvHist.Insert() <> Level:Benign
STOP(ERROR())
END !end if
END !end if
OF ChangeRecord
IF SAV:BackOrder = FALSE
PRO:QuantityInStock += SAV:Quantity
PRO:QuantityInStock -= NEW:Quantity
IF Access:Products.Update() <> Level:Benign
STOP(ERROR())
END
InvHist.Date = TODAY()
INV:ProductNumber = DTL:ProductNumber
INV:TransType = 'Adj.'
INV:Quantity = (SAV:Quantity - NEW:Quantity)
INV:Notes = 'Change in quantity purchased'
IF Access:InvHist.Insert() <> Level:Benign
STOP(ERROR())
END !end if
ELSIF SAV:BackOrder = TRUE AND DTL:BackOrdered = FALSE
PRO:QuantityInStock -= DTL:QuantityOrdered
IF Access:Products.Update() <> Level:Benign
STOP(ERROR())
END !end if
INV:Date = TODAY()
INV:ProductNumber = DTL:ProductNumber
INV:TransType = 'Sale'
INV:Quantity =- DTL:QuantityOrdered
INV:Cost = PRO:Cost
INV:Notes = 'New purchase'
IF Access:InvHist.Insert() <> Level:Benign
STOP(ERROR())
END !end if
END ! end if elsif
OF DeleteRecord
IF SAV:BackOrder = FALSE
PRO:QuantityInStock += DTL:QuantityOrdered
IF Access:Products.Update() <> Level:Benign
STOP(ERROR())
END
INV:Date = TODAY()
INV:ProductNumber = DTL:ProductNumber
INV:TransType = 'Adj.'
INV:Quantity =+ DTL:QuantityOrdered
INV:Notes = 'Cancelled Order'
IF Access:InvHist.Insert() <> Level:Benign
STOP(ERROR())
END !End if
END !End if
END !End case
EMBED: %WindowManagerMethodCodeSection Init (),BYTE 6500
!Initializing a variable
SAV:Quantity = DTL:QuantityOrdered
SAV:BackOrder = DTL:BackOrdered
CheckFlag = False
EMBED: %WindowManagerMethodCodeSection Init (),BYTE 8030
IF ThisWindow.Request = ChangeRecord OR ThisWindow.Request = DeleteRecord
PRO:ProductNumber = DTL:ProductNumber
Access:Products.TryFetch(PRO:KeyProductNumber)
ProductDescription = PRO:Description
END
EMBED: %WindowManagerMethodCodeSection Init 4000
!Updating other files
DO UpdateOtherFiles
EMBED: %WindowManagerMethodCodeSection 8500
! After lookup and out of stock message
IF GlobalResponse = RequestCompleted
DTL:ProductNumber = PRO:ProductNumber
ProductDescription = PRO:Description
DTL:Price = PRO:Price
LOC:QuantityAvailable = PRO:QuantityInStock
DISPLAY
IF LOC:QuantityAvailable <= 0
CASE MESSAGE('Yes for BACKORDER or No for CANCEL',|
'OUT OF STOCK: Select Order Options',ICON:Question,|
BUTTON:Yes+BUTTON:No,BUTTON:Yes,1)
OF BUTTON:Yes
DTL:BackOrdered = TRUE
DISPLAY
SELECT(?DTL:QuantityOrdered)
OF BUTTON:No
IF ThisWindow.Request = InsertRecord
ThisWindow.Response = RequestCancelled
Access:Detail.CancelAutoInc
POST(EVENT:CloseWindow)
END !If
END !end case
END !end if
IF ThisWindow.Request = ChangeRecord
IF DTL:QuantityOrdered < LOC:QuantityAvailable
DTL:BackOrdered = FALSE
DISPLAY
ELSE
DTL:BackOrdered = TRUE
DISPLAY
END !end if
END !end if
IF ProductDescription = ''
CLEAR(DTL:Price)
SELECT(?CallLookup)
END
SELECT(?DTL:QuantityOrdered)
END
EMBED: %ControlEventHandling ?DTL:QuantityOrdered Accepted 8800
!Initializing a variable
NEW:Quantity = DTL:QuantityOrdered
!Low stock message
IF CheckFlag = FALSE
IF LOC:QuantityAvailable > 0
IF DTL:QuantityOrdered > LOC:QuantityAvailable
CASE MESSAGE('Yes for BACKORDER or No for CANCEL',|
'LOW STOCK: Select Order Options',ICON:Question,|
BUTTON:Yes+BUTTON:No,BUTTON:Yes,1)
OF BUTTON:Yes
DTL:BackOrdered = TRUE
DISPLAY
OF BUTTON:No
IF ThisWindow.Request = InsertRecord
ThisWindow.Response = RequestCancelled
Access:Detail.CancelAutoInc
POST(EVENT:CloseWindow)
END !
END !end case
ELSE
DTL:BackOrdered = FALSE
DISPLAY
END !end if Detail
END !End if LOC:
IF ThisWindow.Request = ChangeRecord
IF DTL:QuantityOrdered <= LOC:QuantityAvailable
DTL:BackOrdered = FALSE
DISPLAY
ELSE
DTL:BackOrdered = TRUE
DISPLAY
END !end if
END !end if
CheckFlag = TRUE
END !end if
EMBED: %ControlEventHandling ?DTL:QuantityOrdered 6000
!Calculate all totals
DO CalcValues
To be fair , this is actually a pretty old example, and I don't think it's one SoftVelocity has ever put forward as a best practices standard. It does, however, look a lot like a whole lot of Clarion code I've seen (and written), and it's probably pretty familiar to you as well.
...
- It's difficult to code and to modify. You can't edit this code all by itself, the above combined listing notwithstanding; instead, you're dealing with different embed points at different locations in the generated source. The embeditor does make life a little easier, but you're still paging through code that really isn't relevant to the invoice business logic.
- It isn't reusable. You can't easily take this code and use it somewhere else, say in a modified version of this form as required by a different client, or maybe in some back end code for a web app.
- It isn't testable. You really have no way of knowing if, say, the tax calculation code works other than to try out the form.
- It's difficult to understand, in part because you have to infer what's happening by the location where code is executing.
...
- For instance, you only know that a block of code handles a quantity change because it's in an embed point for a quantity field, and then only because the field equate provides the clue.
Yes, this example is a mess. Yet there's something quite valuable buried in all this code. It's not the database access; it's not the way the screen is updated. The valuable bit is the logic behind a particular way of handling invoicing.
...
Think of your application as made up of three parts:
- the user interface
...
- the data store (TPS files, SQL tables)
...
- the business logic
...
That's a slight oversimplification, to be sure, an oversimplification because there can be some aspects of your application that don't fit nearly into those categories. But it's a useful categorization because it helps to focus attention on the part of your application that is truly unique.
What makes your application valuable to your clients? Is it the user interface (the UI)? Possibly, but unless you write your own UI layer then chances are other applications out there also in your market segment that use similar controls, if in different arrangements. For most of us (especially as Clarion developers), the UI is not what makes our customers open their wallets.
Is it the data layer? If you use TPS files you have certain advantages and disadvantages over compared to developers using other database systems, but unless you're selling specialized data access or data mining where the key is the speed and power of the database, it's not going to be your data layer that gives your application its unique value.database, it's not going to be your data layer that gives your application its unique value.
I think of user interface components and storage systems as commodities - they're things you buy, they're readily available, and while they have great utility they're also pretty much replaceable. Every business software development system I've seen gives you a way to create a UI and access data. Big deal.
In almost all cases what gives a business application unique value is the business logic. That logic has to fit with how your customers do their business; the more flexible, the more reliable your business logic is, the more robust your application.
Imagine the Invoice.app is your own product (stay with me here). When you create invoices, applying taxes and discounts as appropriate to your industry, that's your business logic. When you manage inventory movement according to the unique requirements of your shipping system, that's your business logic.
And here's the thing: for the most part, Clarion isn't much help when it comes to writing code that embodies all that uniquely valuable business logic.
That's right. Clarion is next to useless when it comes to creating the most valuable part of your application. It's no better than any other environment, and actually worse than some!
But Clarion is enormously useful at handling all the grunt work of creating menus, browses, forms, reports etc. It does all that work so you can devote most of your energy to writing the stuff that really matters. That's Clarion's true competitive advantage.
...
I won't, however, be tackling the Invoice example until a little later on in this series. I've showed it to you because I think it's a fairly typical example, and I want you to know that's where these articles are headed. But it's also problematic because it's so tightly tied to the database and the UI.
Instead, I'm going to ease into this with a simpler example. And it's kind of a funny one.
...
This isn't an easy task. But as it happens I took it on two and a half some years ago when I ran a series of articles on the most popular embed points, based on data extracted from a number of reader-submitted TXAs. That code wasn't specifically concerned with the contents of the embeds, just whether or not they were used. But it wasn't that hard to adapt the code to extract the embedded source itself.
...
The template advocacy statement I like is this one: "
As long as your code isn't critical, unique business logic, the second time it goes in a template. Otherwise put it
...
into a class, with one class per source file, using descriptive naming.
I'll come back to the descriptive naming later in this series.
Templates aren't an optimal solution for most business logic, or really for any complex logic. If you try to treat the template language as a business programming language , you'll quickly become frustrated by the quirky syntax, the need to mix Clarion language statements with template language statements , and the difficulty of debugging, etc.
The template language's job isn't to take the place of the Clarion language, it's to make it easy to generate repetitive Clarion code. And core business logic is generally not repetitive.
...