Blog

  • Untitled post 36634

    LucaP The Chart of Accounts User interface.

    #LucaP

    Introduction

    I’m building an ERP system from scratch. I’m on this journey to explore what happens under the hood in a classic ERP system, going module by module and showing you what you should be thinking about as you use them in more mundane ways, like customizations, integrations, and, of course, purchasing new systems. Last time we built the model for the Chart of Accounts, and it’s time to implement that in the user interface. We’ll review the finished template, why to use a template, add a few more fields, and then connect everything to make a usable Chart of Accounts module.

    The Template

    I spent a good part of the last year putting together the Luca ERP template. If you are using a database for a backend platform – and that is a very wise thing to do – about two-thirds of that work was unnecessary, as the elements of CRUD were already there. CRUD stands for Create, Read, Update and Delete, the four functions one needs to manipulate a set of data. I took the rough road and built all of that in SwiftData from scratch, for a few reasons, including promoting my course SwiftData essential training in the LinkedIn Learning library. More importantly, I wanted to get my hands dirty on building that back end.

    Unfortunately, it took much longer than I expected due to a bug in the architecture of what I built. I discuss that in a previous newsletter if you are interested. Once I built that, I got the user interface together, changing my mind along the way from an unwieldy style system of fixed fields to using the Grid view of SwiftUI to simplify everything.

    Why have a template? The answer is consistency and focus. The User interface for all parts of the application will look the same. More importantly, all the hard-to-do functions in each module, such as CRUD, are implemented once and applied to every module. For each module, I change the model, the user interface, and any small customizations for working with the model in a new copy of the template, then have a new module ready for production.

    The template with demo code looks like this when I run it.

    The title, toolbar, and bottom bar are all template code. They don’t change. The toolbar runs some functions in my code, and I will modify them if necessary.

    In between is the body. There are three parts of the body code:

    1. The Form – this is one style of data presentation, used for a single record/row.
    2. The Table – this is another style of data presentation, used for multiple rows. You’ll see this as a child table to a form in a one-to-many relationship. The table is also the style for most reporting.
    3. The footer or summary – Used in conjunction with the table, it contains summary information for the columns in the table, such as sums, counts, or averages, though it could contain more. For example, on invoices, the table may contain line items, but a few parent fields, such as freight and taxes, might appear here.

    Since consistency is part of the game, I also built a style sheet into the system. If my user interface were a web page, it would be in CSS. SwiftUI can create custom modifiers, which I discuss in detail in Chapter 12 of The Complete Guide to SwiftUI. I built custom colors and styles for Grid body elements for titles and fields, and styles for large and small buttons.

    //
    //  LPStyleSheet.swift
    //  LucaPSwiftUI
    //
    //  Created by Steven Lipton on 12/26/25.
    //
    
    import SwiftUI
    
    struct SmallButtonStyle: ViewModifier{
        func body(content: Content) -> some View {
            content
            .padding(5)
            .frame(width:25,height:25)
            .bold()
            .foregroundStyle(.pitchBlack)
            .border(.pitchBlack)
            .background(.midMist)
        }
    }
    
    struct SmallGridButtonStyle: ViewModifier{
        func body(content: Content) -> some View {
            content
            .padding(6)
            .frame(width:32,height:25)
            .bold()
            .foregroundStyle(.duskBlue)
            .background(.mist)
            .clipShape(RoundedRectangle(cornerRadius: 3))
        }
    }
    
    struct ButtonStyle: ViewModifier{
        var foregroundColor: Color
        func body(content: Content) -> some View {
            content
            .padding()
            .bold()
            .foregroundStyle(foregroundColor)
            .border(foregroundColor)
            
        }
    }
    
    struct TitleGridStyle: ViewModifier{
        var alignment: HorizontalAlignment
        func body(content:Content) ->some View{
            content.gridColumnAlignment(alignment).bold()
        }
    }
    
    struct FieldGridStyle: ViewModifier{
        var alignment: HorizontalAlignment
        var borderColor: Color
        func body(content:Content) ->some View{
            content
                .gridColumnAlignment(alignment)
                .padding(5)
                .border(borderColor)
                .fontWeight(.regular)
        }
    }
    
    extension View{
        var smallButtonStyle: some View{
            self.modifier(SmallButtonStyle())
        }
        
        var smallGridButtonStyle: some View{
            self.modifier(SmallButtonStyle())
        }
        
        func buttonStyle(foreground:Color = .pitchBlack)-> some View{
            self.modifier(ButtonStyle(foregroundColor: foreground))
        }
        
        func titleGridStyle(alignment:HorizontalAlignment = .trailing) -> some View{
            self.modifier(TitleGridStyle(alignment:alignment))
        }
        
        func fieldGridStyle(alignment:HorizontalAlignment = .leading,borderColor:Color = .midMist) -> some View{
            self.modifier(FieldGridStyle(alignment:alignment,borderColor: borderColor))
        }
        
    }
    
    extension ShapeStyle where Self == Color{
        static var pitchBlack: Color{
        Color(red:0.078,green:0.075,blue:0.004)
        }
        
        static var darkRed: Color{
        Color(red:0.573,green:0.078,blue:0.047)
        }
        
        static var midMist: Color{
        Color(red:0.808,green:0.875,blue:0.851)
        }
        
        static var duskBlue: Color{
            Color(red:0.180,green:0.314,blue:0.467)
        }
        
        static var mist: Color{
        Color(red:0.922,green:0.988,blue:0.984)
        }
    }
    
    

    Finally, there are body elements, namely buttons, that will have some function related to the table I’m using. selection, deletion, and inactivation of a record are the three functions, and I’ll uncomment them as needed for the module

    All this lets me concentrate more on the module’s function and less on management or UI elements. I’ll make a copy of the template, rename it, and start populating it with the relevant information for the module.

    More Fields

    Before we go any further with all this, I want to take one step back. I mentioned roadmaps earlier this week. The roadmap for the next few newsletters to put the basic general ledger together actually looks like this:

    I interlaced infrastructure into the general ledger so we have a context for those topics. In a normal development environment, I’d do those first. However, for LucaERP, having something to work with in each case will make this a lot easier to understand.

    What I can see from this roadmap, however, is that there are a few things I may want to think about in advance. Finding my cash accounts for the cash flow report is one example. If other fields may be useful, adding them now saves work later. We’ll deal with data migration much later, but if I can prevent a migration problem up front, I’ll try to do so.

    Another issue is the control account in reporting. I called them summary accounts last newsletter, but the technical term is control account. I can use some sophisticated logic to use the levels as I originally intended, but that’s a lot of work. It is much easier to have a flag indicating that something is a control account, and to handle these accounts in reporting and entry differently from transactional accounts.

    So I’m adding two more flags to my model:

    // more properties of the account for reporting purposes
        var isControlAccount:Bool = false
        var isCashAccount:Bool = false
    

    Add the Model

    We’re ready to add the models I created to the template. I made a folder for them in my application, and I can add them there.

    Once the models are in, I register them with SwiftData:

    .modelContainer(for: [
               PModelPlacemarker.self, //used as a placemarker for the parentmodel
               CModelPlacemarker.self,  //used as a placemarker for the child model
               LPChartOfAccounts.self, // G/L - Chart of Accounts
               LPCompanyLocation.self, // G/L - Company Locations
           ])
    

    Add the Fields

    I’ll make a copy of the template next and name it LPChartOfAccountsView.

    For the two primary tables, I set up a typealias for the one I’m using. There’s a placeholder if I am not. In this case, I only have a parent model that will use the form, so I’ll change ParentModel and leave ChildModel with the placeholder.

    typealias ParentModel = LPChartOfAccounts
    typealias ChildModel = CModelPlacemarker
    

    Next, I’ll add the properties from LPChartOfAccounts to the view that I’ll be using. I don’t need the protocol properties—just the table ones.

    @State var accountType: Int
    @State var level1:Int
    @State var level2:Int
    @State var level3:Int
    @State var location: Int
    @State var accountLevel: Int
    @State var accountName: String
    @State var createDate:Date = Date()
    @State var modifyDate:Date = Date()
        
    // more properties of the account for reporting purposes
    @State var isControlAccount:Bool = false
    @State var isCashAccount:Bool = false
    

    These will be our display copies.

    Several reset and refresh methods transfer data from the table to these display variables, or reset them. I’ll fill out those methods next.

    Here’s the reset method filled out:

    func resetCurrentData(){
    //TODO: add current data to reset here, use "", Date, etc. to reset
        accountType = .asset
        level1 = 0
        level2 = 0
        level3 = 0
        location = 0
        accountLevel = 3
        accountName = ""
        createDate = Date()
        modifyDate = Date()
            
    // more properties of the account for reporting purposes
        isControlAccount = false
        isCashAccount = false
    // reset the key
        currentKey = 0     
    }
    

    I’ll take that from the parent record and populate my display variables when I load a parent record:

    func refreshData(key:Int){
    	if let parentRecord = parentRecord(key:key){
    		currentChildSavedMax = parentRecord.childSavedMax
    //TODO: Place the current data for the parent record here into state variables
        accountType = parentRecord.accountType
        level1 = parentRecord.level1
        level2 = parentRecord.level2
        level3 = parentRecord.level3
        location = parentRecord.location
        accountLevel = parentRecord.accountLevel
        accountName = parentRecord.accountName
        createDate = parentRecord.createDate
        modifyDate = parentRecord.modifyDate
                
    // more properties of the account for reporting purposes
        isControlAccount = parentRecord.isControlAccount
        isCashAccount = parentRecord.isControlAccount
    } else { // place error data
        resetCurrentData()
        accountName = "Error - Record not found"
    }
    //not needed in this case, but left with the blank placeholder. 
            currentChildren = findChildren(parentKey: currentKey)
        }
    

    Display the Data

    The data will be displayed in a grid. I only need the parent part this time, so I added the fields. In doing so, I realized I had forgotten something in the template: inactivating fields. This turns out to be more of a style sheet issue than a template issue, so I add a flag to the field modifier to inactivate and change the appearance of an inactive field.

    Once added, it looks like this in the create mode:

    When we inactivate fields is important. Deletion or changes to account numbers are not good for audit trails or transactions that depend on this account, so I’m going to prohibit them. Only in creation can you make an account number. Deletion will be forbidden. For updating, you can change the name only.

    I can make a function that returns whether a field should or should not be active under those conditions:

    func activeField(isActive:Bool) -> Bool{
    	return isActive && (currentMode == .create || currentMode == .update)
    }
    

    User-defined Keys

    While the template was based on system-defined keys for each record, the Chart of Accounts is a user-defined key situation. Instead of incrementing to the next-largest value, the user will assign the key.

    To make it more complex, the key consists of five fields. As we discussed last time, that means making a function to generate this account number. As we need an integer for the primary key, our version went like this:

    var accountNumber: Int{
            let actType = accountType.rawValue * 1000000000000
            let l1 = level1 * 1000000000
            let l2 = level2 * 1000000
            let l3 = level3 * 1000
            let loc = location
            return actType + l1 + l2 + l3 + loc
        } 
    

    As I change the five fields, I’ll update the current key to account number. For example, I did this to the accountType

    Text("Level 1").titleGridStyle()
    TextField("Level1", value: $level1, formatter: threeDigitFormatter)
        .fieldGridStyle(isActive: activeField(isActive:currentIsActive))
        .onChange(of:level1){ currentKey = accountNumber}
    

    With this in place for each field, the currentKey, which I’ll save in create, will set the correct account number as the key.

    Despite this user-defined key, you will find in many modern systems, and something SwiftData does for you: there is another key called id besides our user-generated one. SwiftData always generates a unique identifier, a UUID, for every record. If we can ensure the user key won’t change by prohibiting changes, we can use id for much faster, easier searches and references. We’ll come back to this later and in the transaction journal, but I’ll refer to each account through account.id instead of account.key in the journal.

    The Chicken before the Horse: Credits and Debits

    Dependencies can work both ways and can cause problems when figuring out the order to build modules. It is both a chicken-and-egg problem and a cart-before-the-horse problem, so I refer to it as a chicken-before-the-horse problem: Which exists first, and in what order do I place things? We run into that with a feature I want to add to the Chart of Accounts: the total debits and credits for that account. That would be the total from the journal transactions, but I haven’t made any yet, and I can’t make them without the Chart of Accounts.

    TO handle this, I do the primary function first: get the Chart of accounts running without totals. Totals are a decoration on top of that, so they have a lower priority. I’ll come back later and add totals once the transaction journal is up and running.

    Add the Actions

    Several actions need to be changed in the template. Create, Delete, and Update work very differently from what the template assumes.

    For a user-defined key, we won’t use all the savedMax and currentKey stuff when creating. Instead, we have something very simple:

    currentKey = 0
    

    It starts blank every time, and changes during entry. Every time we submit a new record, we’ll check for a duplicate key. If there is one, we reject the submission and send an error.

    Once we have transaction entries, there will be many dependencies on the Chart of Accounts. For that reason, I never want to delete a chart of account entry. However, users may have added an account in error. We might still want to delete an account that has never been used. We have the chicken-before-the-horse problem again: we need to see transactions to determine whether the account was used. For now, we prohibit all deletions of accounts.

    I could set a flag for this in the Chart of Accounts that gets turned off at the first transaction, but I prefer a different approach: a deletion utility. This approach is a security measure. No user but a superuser or administrator should be able to delete an account. I’ll eventually add it to the utilities and settings module, which I’ll control by user privilege level.

    As far as users are concerned, the only thing in the delete is a message you cannot delete.

    errorMessage = "Deletions not allowed in Chart of Accounts"
    

    The last change is to update. Much of the table is columns to create the account number, which we don’t allow. The only two things that we’ll allow to change after creation are the name and whether it is a cash account. These only affect reporting and do not cause damage to the accounts.

    The Okay Button

    Much of the change for create, update, and delete occurs in the OK button actions. In create mode, we check for a unique key, create a new record, populate it with our key and display properties, and display it. We insert the record into the table.

    func appendParentRecord(){
            //check for uniqueness
            if parentModel.contains(where:{$0.key == accountNumber}){
                errorMessage = "Account already exists"
                return
            }
            
            let newRecord = ParentModel()
            //TODO: Insert the properties to save into `newRecord.`
            // the protocol ones
            newRecord.key = accountNumber
            
    
            //columns in the table
            newRecord.accountType = accountType
            newRecord.level1 = level1
            newRecord.level2 = level2
            newRecord.level3 = level3
            newRecord.location = location
            newRecord.accountLevel = accountLevel
            newRecord.accountName = accountName
            newRecord.createDate = Date()
            newRecord.modifyDate = modifyDate
            newRecord.isControlAccount = isControlAccount
            newRecord.isCashAccount = isCashAccount
    
            //insert the new record
            modelContext.insert(newRecord)
        }
    

    The original plan for Update mode does not work here. First of all, there’s a bug I hadn’t thought about. I deleted and inserted the new record. That will change the value of id and mess up my plans for’ id’ in othere tables as a primary key. I need to grab the record, modify it, and save it instead.

    I am also changing only two things here, so I only do the assignments for those two.

    Finding records

    The last function we’ll deal with is find. I’ll want a way to jump to an account quickly. Once set up, there will be many accounts. I’m going to use the account type to filter the results to that account type, reducing the number in the list. Clicking an item on the list, then clicking okay, displays the appropriate record.

    I’ll display this in another window above my current one,

    One Last Cosmetic Change

    To differentiate between active and inactive fields, I’m using a custom style, but it doesn’t look good on pickers because they dim too much.

    So I’ll change it to use a text field when inactive and a picker when active.

    Text("Level").titleGridStyle()
                        if activeField(isActive:currentIsActive){
                            Picker("Level", selection: $accountLevel){
                                Text("Level 1").tag(1)
                                Text("Level 2").tag(2)
                                Text("Level 3").tag(3)
                            }
                            .fieldGridStyle(isActive: true)
                        } else {
                            Text("Level \(accountLevel)")
                                .fieldGridStyle(isActive:false)
                        }
    

    the result

    And with that, we have our Chart of Accounts. The creation mode looks like this:

    A viewed account looks like this:

    An editing account has a mix of active and inactive fields.

    Next steps

    With that, we’ve done as much as we can on this round. I could start adding accounts, but that will take some time. One of our next two tasks is to set up our first import system, a way to pull data from an external source and populate the Chart of Accounts. We also have to set up utilities for account deletion, addition, and location editing. That requires two more infrastructure pieces before we even get to the import: the menu and the users. In our next LucaP newsletter, we’ll tackle menus and user privileges.

  • Bizoneness and LucaP: SAP B1Web client, LucaP ERP, and making a roadmap for an application

    I’m building an ERP from Scratch called LucaP ERP. For the last two weeks, I’ve had some interesting discussions in both my LucaP and SAP Business One Web client, which I wanted to clarify once and for all. I decide to take an extra column to do so, and then have the LucaP column show later in the week.

    What LucaP ERP is for

    One of the biggest confusions is what Luca ERP is and why I’m writing this, since there are plenty of solutions out there.

    LucaP is built to be an educational product, not a functional ERP. It’s a demonstration piece, meant to be open-source when I get far enough into creating it.

    We cannot look at all the decisions and code of any published Application that violates Intellectual property laws to take a look under the hood (for more on that and the way forward outside of the US, I suggest this video by Corey Doctorow). Building this from scratch is the best way to explore a range of topics related to ERPs.

    You’ll often hear about high failure rates of ERP implementation. People don’t ask the right questions either as customers, consultants, or developers. Many people don’t even know the questions, believing that any ERP is like Excel and one size fits all, which is very far from the truth.

    Sun-Tzu’s advice at the conclusion of Chapter 3 of The Art of War applies to any big venture.

    Know your enemy and know yourself, and you will win many battles. Know yourself, and you will win half the time Know neither, and you are bound to lose.

    The enemy here is the ERP implementation; you are your company, its culture and practices, both official and unofficial. You need to know both, but in this case, the enemy is often deceptive behind a few promises of a sales representative. Knowing how to ask the right questions to make the right fit is a powerful skill that leads to success.

    LucaP is about asking those questions from the bottom up. It requires me to show everything and make some of those decisions while asking the questions you should be asking. IT is not about having a working ERP at the end, but knowing the right questions to ask to get or build one. If you are customizing a current system, these are questions you should be asking as well.

    This is a long term project, especially when I"m working on it biweekly.

    My principles of engineering

    Another reason this takes so long is my principles of software engineering. There are plenty of models, but many include these parts.

    If I came across as critical about SAP last week, it really came down to three principles I find vital for any project, all three of which I’m using in my principles for LucaP ERP.

    The first is dependencies. Some modules will depend on other modules. You cannot have a general journal without a chart of accounts. On a higher level, you must have a general journal before having the Banking, Inventory, Accounts payable, and Accounts receivable modules. These modules depend on Inventory, as does the bill of materials, which, in turn, the production order depends on.

    For ERP, I map out my modules at the top level, then build down from there. Here’s one example of such a plan.

    This gives me a rough roadmap to see the most critical dependencies. It’s a big onion, with General Ledger in the center and the modules that most directly access it. For Assets, we have Accounts receivable, Inventory, and Banking (Cash). For liabilities, Accounts payable. Outside of that, there are additional dependencies on A/R, A/P, and Inventory. Some are debatable. How to categorize Payroll, for example, may be as an A/P function, a separate module, or not at all, and use a separate service for the whole thing with a few G/L accounts.

    A similar case is SAP Business One’s Business Partners, where all contacts are in one module, though most systems split this into smaller parts: vendors as part of A/P and customers in A/R. These are the kinds of decisions developers make; I’m looking to highlight them. Those looking at systems for purchase, migration, integration, or customization must understand the implications of such decisions before proceeding.

    This map then contains smaller maps that provide the functions, tables, and reports necessary for the module to operate at its minimum level. This is referred to as the minimum Viable product (MVP). The goal is to get something that meets the requirements of an application or module. I’m treating each module as a product in this scenario, and I need to meet specific requirements to be viable. For the general ledger, that would be something like

    1. Provide a chart of accounts that can distinguish the account types of Assets, Liabilities, Revenue, operating expenses, and Costs of goods sold.
    2. Provide a transaction record (the general journal) of all debits and credits to accounts, where each transaction record has credits equal to debits.
    3. Provide a report to summarize the accounts to detect imbalances between credits and debits(Trial Balance) for a given period.
    4. Provide a mechanism to correct errors and variances in credits and debits that provides a traceable log.
    5. Provide reporting to show the financial health of a company (Balance Sheet, Profit and Loss statement, Cash flow ) in compliance with accounting standards.

    Sometimes you may go beyond the requirement and add extra features, knowing they will be necessary for other dependencies. The levels system I created in the chart for the account is not mentioned in requirement 1, but it makes requirements 4 and 5 much easier to meet.

    My problem with SAP in the last newsletter was that they did not deliver a viable product in the accounting module, which was a pattern I also saw in the production module. #4 and #5 in my requirements are missing, and most modern accounting practices would require all of them. 1-4 go back to Luca Pacioli’s original text on accounting from 1498: the general journal, a chart of accounts (though admittedly his was not as organized), a trial balance, and correction of accounts based on a trial balance.

    Ship early and ship often is a good idea, but ship something viable, not half-baked. To do that requires a roadmap, and making it public keeps you honest.

    I have a roadmap to keep me honest, though I’m also aware of the quote.

    Plans are perfect until engagement with the enemy.

    I will need to make changes to the plans, namely by breaking up one idea into multiple ideas. The last Luca ERP ended up being the model, while the next one is the User interface, though I hoped for both in the same column. That will happen, but I do have a plan for how to do this. I believe in transparency with this stuff. A public roadmap does not make you vulnerable to your customers. In some cases, yes, they might copy an idea. But it shows one’s integrity in completing each part in a logical order.

    Here’s LucaP ERP’s roadmap as I have it planned out now.


    2025

    • UI Template (complete)

    2026

    • General ledger
      • Chart of accounts
      • General Journal
      • Trial balance
      • Vouchers
      • Financial reports
        • Balance Sheet
        • Profit and Loss Statement
        • Cash Flow
    • Infrastructure components
      • Settings
      • Menus
      • Users
      • Data Import tools
    • Banking (note: limited design due to security issues)

    2027

    • Inventory (TBD – may combine some of these or leave them separate)
      • Quality Issues
      • Finished goods
      • Raw materials
      • Work in progress (sub assemblies)
      • Labor
      • Service
      • Fixed Assets
      • Inventory transfer.
      • Depreciation
      • Inventory reporting
    • Accounts Receivable
      • Customers
      • Sales Orders
      • Invoices
      • A/R reporting
        • Aging

    2028 and beyond

    • Accounts Payable
      • Vendors
      • Purchase orders
      • A/P Invoices
      • A/P Reports
    • Production
      • Quality Issues
      • Bill of Materials
        • Fixed Costs
      • Production Orders
      • Release to Production
      • Routing Slips & Quality
      • Close Production orders
    • Logistics
      • Pick and pack
      • Packing Lists
      • Shipping labels
      • EDI (for future reference)
      • Hazmat/Dangerous goods
    • MRP

    This is not complete by any means. The important part of this list is the primary categories, such as General Ledger, Inventory, and Infrastructure. They are in dependency order, with the most important first. What is important to me is the MVP -> MVP design: moving from a minimal viable product to the most valuable product. Make something that meets the minimum of your requirements, get it working, and publish. That’s a minimum viable product. I do that module by module. In a production environment, I don’t ship the general ledger until it works to spec. Once it does, I can make improvements until it works better than anything else on the market.

    As this is an educational model of an ERP, I work on a variant of this. I work linearly through each module. Each column brings us closer to an MVP for a module. Much of 2026 will be the general ledger and a subset of banking, and if I’m lucky, the beginnings of Inventory, the next highest dependency on the list. I have one exception here, which, again, is a function of being an educational model: there are infrastructure elements that should have been developed first, such as user logins, menus, settings, etc. I would in a production piece, but for educational use I need these in context, so they do get interlaced with other material in my internal roadmap of columns to write.

    Last week’s discussion of the accounting module in the SAP B1 web client is an example of what not to do. The product did not meet the requirements I posted above, namely, the reporting features were missing. Now, to be fair, SAP is working from a different point than I am. The web client is a new user interface and user experience for an existing set of tables, built on a nearly quarter-century-old user experience. Yet, at least outside SAP, there is no sign of a roadmap for the web client. We do have an infrastructure in place for tiles, forms, and reports. Yet, we have modules randomly appearing in the web client; some work, and others, like production orders, lack critical features. There is no discipline to complete one module to a minimal viable product.

    There’s a big difference between a multinational organization with myriads of siloed departments and products and a single indie developer and educator like myself. Organizational weight is, I’m sure, a major issue for SAP. Production efficiency and discipline are lost in organizational bureaucracy, while I, as an indie developer, am unburdened by such things, leading to a different level of efficiency.

    LucaP ERP is an educational product designed to raise issues found in ERP systems. There is no one-size-fits-all ERP, but knowing how to measure is helpful for someone migrating to a new system. Knowing how to ask questions to understand what is going on under the hood helps with customization and report writing. LucaP is a serverless ERP meant to ask those questions. It is different from a functional ERP, like SAP Business One, which I actually enjoy using and which has some design decisions that are brilliant, if unorthodox, for the industry.

    Application design is not easy. I’ve touched the tip of a much bigger iceberg of topics, but hopefully enough to clarify for my readers the goal of this project and how it differs from other, more professional projects. I’m an educator and wether you are interested in swift and have taken my Complete guide to Swift UI or in SAP Business One and taken my SAP Business One Essential Training in the LinkedIn learning library, I’m doing these columns for everyone in those realms to get some knowledge of the art Data processing, one I am afraid will be lost in the belly of LLMs. I intend LucaP to be a long journey, but one I hope you follow with me as we really think out the issues behind ERPs and Data Processing.

  • BizOneness: The Accounting Module in the SAP B1 web client

    BizOneness: The Accounting Module in the SAP B1 web client

    Over the last few BizOneness columns, I’ve been covering the SAP Business One web client, the latest advancement in SAP Business One’s user experience. This time, I’d like to look at the accounting module.

    To catch up readers who recently started reading this newsletter, I launched BizOneness as a biweekly newsletter focusing on SAP Business One features, tips, and tricks. About a year ago, in the gaps between newsletters, I started a new newsletter dedicated to understanding what’s going on under the hood in an ERP and the decisions developers, designers, and you, as a buyer and user, make when getting an ERP. Under the current Digital Rights Management law, showing the code for an existing ERP such as SAP B1, Oracle, or QuickBooks is illegal. Instead, I started writing my own open source ERP called LucaP ERP, named for the author of the first accounting textbook. The code and ERP created are not production code nor even useful as an accounting system. It is a model for looking at issues in an ERP and answering the question “why did the developers do that” by understanding the questions the developers answered. Converesely it gives developers the context to understand what they should be doing in the world of business platforms.

    I’m in an interesting case this week where I’m paralleling the LucaERP articles with reviewing the accounting modules in the SAP web client. It thematically goes together, and once again I’m left to ask about SAP, “What were they thinking?”, though this time I’m not sure of the answers.

    So yes, spoiler alert: If you read my reviews of the production module, you are in for more of the same.

    Instead of a finance module as in the traditional client, the web client has accounting.

    Let’s look at each module, starting with the Chart of Accounts. Clicking the tile for the Chart of Accounts provides what looks a lot like a balance sheet.

    Of course, it is missing time periods, and the filters have no time. The total is the lifetime balance found in OACT.CurrTotal. I’ve never been clear how accurate that is, as I haven’t figured out how and when it updates.

    There is a level button in the upper-right corner that lets you adjust the level of detail shown.

    You can also drill down using the arrows. I’ll drill down on the cash accounts.

    You can get to the details as well, which pops up another pane of detail within the same window.

    While it might look cool, there is no way to dismiss this second pane, which can be annoying when selecting accounts, as the selection pane is often too small to see a full description or account number. Your only option is resizing the pane back and forth to see the information you need. There needs to be a hide button for the detail pane.

    The Chart of Accounts has the basic information you’d expect. although not marked after the account, and there are fields for division, region, and department

    Under properties, you can signify a control or cash account, among other switches. 

    A second tile for the Account Code Generator lets you create a Chart of Accounts and specify relevant information. 

    Moving on to Journal Entries, we can get a list of entries. 

    Most entries have a source, such as an A/P or A/R invoice. We don’t often enter journal entries directly, but we look at them. 

    I’ll pull one up for an A/P Invoice.

    We get a parent/child type view. The parent provides dates, sources, and references for the journal entry. The transactions are below, broken down by accounts used and debits and credits. I’m such an old-school accounting type that I like to keep everything under credits and debits so I can be sure they balance for the five hundred years of double-entry accounting that’s been the point. But apparently, SAP thinks that’s obsolete thinking, or their developers and designers have no interest in keeping debits and credits equal. 

    And sometimes those numbers are not equal, and we have to fix them. Vouchers are the tool for doing so, and we do have them in the web client. 

    And while this is all fine and good, it is also totally worthless for most accounting and finance uses. There are no financial or accounting reports here—no Profit and loss, balance sheet, or cash flow. 

    And while I can see uses of keeping those locked up somewhere for security and company privacy purposes, trying to balance the books without a trial balance is another thing altogether. It is completely missing: I cannot find it anywhere. 

    I remember one birthday when I was young, I got the hottest new electronic toy. I was so excited until two things became clear: first, we didn’t have the right batteries in the house. What was worse was the snowstorm that started that morning, which made it impossible to get those batteries for a few days. I was not a happy little boy.

    SAP Web client reminds me of that toy. I so want it work beautifully, but like the batteries, everything in the accoutning module is all meaningless without that trial balance report, like no batteries for a toy. With the toy, the best I could do is pretend to use it. And we could adapt for SAP. We can get summaries from the Journal entries, but these are lifetime summaries, and with no filter, we cannot view only a period. 

    A while ago, I wrote a bizoneness newsletter about the general journal. An adaptation of that report is this formatted for HANA, but should work on SQL: 

    SELECT
    T2."ActId",
    T2."AcctName",
    T0."RefDate",
    T1."Debit",
    T1."Credit",
    T2."Postable",
    T2."Levels"
    FROM
    OJDT T0
    INNER JOIN JDT1 t1 On t0."TransId" = t1."TransId"
    INNER JOIN OACT t2 ON t2."AcctCode" = t1."Account"

    This is a data source added to the user-defined queries. From it I can make a view to give me something like a trial balance, including a transaction date I can use to filter to a period.

    However, Due to the massive number of transactions, it faces many problems. You get a warning that Select All is not available for more than 5000 records. 

    Until you manually group by Account number and filter by posting period, the report will not work well. Doing so gets you this: 

    And it lets you drill down from there: 

    Notice that it is only accounts with a transaction. More work would be needed to get all accounts or control accounts. 

    If you went back to the earlier Bizonenss Newsletter, you’ll find that it took me three newsletters to develop. There’s a lot to the Chart of Accounts and the general journal, and it takes some expertise to put reports there together. The report still lacks many variables including multiple currencies. For a critical report, users should not have to make their own like this. 

    Once again, SAP has left me irritated and annoyed. I keep wondering why they build something that doesn’t work. The web client was the supposed answer to the user experience that has not changed in over twenty years. I’m only partially joking when I say the current client for SAP Business One is high tech for the era of Windows ME. And while the user interface for the new web client is there, and we have some nice charts and tables in analytics, I’m not clear on the roadmap for the web client. Is it a bait and switch to make a nice-looking interface for a few modules that sell the product to new users, and then the users find out the Main client is the only usable one? 

    In some ways, the lack of a trial balance report in the accounting module brings me back to my point about LucaERP. There isn’t a roadmap. The modules look good, but the designers and developers behind them have no accounting or business backgrounds. A developer and designer who doesn’t understand this will not ask the question about whether they should do the trial balance next or take the initiative to write one. 

    SAP, for its part, is not hiring people who have that kind of initiative. Either through subcontracting or internal developers, they don’t seem to care about the user experience, especially when they’re interested in marketing the newest and most expensive shiny toys in the shop. HANA was one of those toys, and as good as it is for databases, I still miss features from SQL such as recursion and SQL’s looser syntax. 

    Supposedly, another large upgrade to the web client is coming in the February 2026 patch. I got my hopes up in the last patch, but I’m quickly losing faith in the web client, even though I’d love to see it succeed. The web client works for a few cases, such as order entry or analytics, but in most modules, like production and finance, it falters like a toy with no batteries. In some cases, it is even worse: batteries but no toy. 

    Like the little kid I was, I wanted the toy to work. I want the web client to be a robust alternative to the current client. There is so much that the old client missed in terms of accessibility alone that web browsers give you for free. The number of devices, from tablets to desktops that can use the web client is extensive when not dependent on one third-party vendor for a client application that could any day (if not already) turn into AI spyware transmitting your financials to competitiors and the government. I want the SAP Business One web client to work, but sadly, it just keeps disappointing me.

    Next Week: LucaERP is back with the implementation of the Chart of Accounts user interface. We also review the data model to ensure nothing was missed.

  • LucaP_ERP 18:The Chart of Accounts Model

    LucaP_ERP 18:The Chart of Accounts Model

    I’m building an ERP system from scratch. When I started this project a year ago, I thought it would be easy, but getting some fundamental parts created took nine months of effort. So we’re back where we were a year ago, working with the essential modules of an ERP system, and we’ll start where everyone should – the chart of accounts.

    The goal of these newsletters is to illustrate all the decisions required to manage an ERP. Not just making an ERP, but if you were to purchase or convert to a different system, or are involved in maintenance, there are decisions you make about the ERP that will critically affect a business, and even determine if the effort for implementing an ERP will succeed or fail. Before everything else, the chart of accounts in an ERP must be correct. In earlier newsletters, I talked about the basics of accounting and the chart of accounts. Here I want to get into details.

    The first problem is one that anyone involved in implementing an ERP system has encountered: how to represent the chart of accounts with unique identifiers for each account. Now we can put in 1, 2, 3, etc., as we’ve done in the template. But that is not effective here. The number will have meaning.

    First of all, let’s remember the accounting equation I’ve been using:

    Assets – Liabilities = Investment + Revenue – (SG&A Expenses + COGS)

    While I could lump Sales, general and administrative expenses (SG&A) and the cost of goods sold (COGS) into just two subcategories of expenses, they are such critical numbers in evaluating efficiency that they get their own categories in the equation. There are other numbers I want to keep separate from all this, such as investments the company makes in different companies. I’ll stick to the basic six we have here.

    Why these are so important for coding is that we can use them as prefixes for any account number to identify its category quickly. But what do we use to code these categories? Here’s a table of two types of codes

    Number code Letter Code Account Type
    1 A Asset
    2 L Liability
    3 E Equity
    4 R Revenue
    5 G SG&A
    6 C COGS

    If I were to use a 0-999 numbering system for each category, I would say the Cash account is A001 or 1001, depending on whether I use an account number or a name. But what would that look like in a database? I have two choices: a string/varchar and an integer/number.

    In Swift, I might have an entry in the chart of accounts like this:

    @Model
    class ChartOfAccounts{
    	var accountNumber: Int
    	var accountName: String
    }
    

    or this

    @Model
    class ChartOfAccounts{
    	var accountNumber: String
    	var accountName: String
    }
    

    Which one is better? The string “Account number” is more flexible, but the integer will be faster in searches.

    I’ve found most systems I’ve looked at use the string setup, but that is not universal. If you are migrating from one system to another, looking at the types of the two systems will save you hours of migration headaches.

    But we are not yet done with this account code problem: that’s only if there is a simple code. What if we want to break code into smaller units? There are different types of Assets, such as cash, inventory, accounts receivable, and equipment. Keeping with cash, we might have even more subdivisions of accounts under cash:

    • cash
      • Cash on hand
      • Bank – Checking
        • Bank #1
        • Bank #2

    You could make it more complex by including accounting for different locations or regions. Suppose the Huli Pizza company operates in two regions: Hawaii and the Mainland USA. We may track performance for each area by assigning accounts to each region. As the mainland may not have as easy access to the Hawaiian banks, they may have their own bank, especially for payroll.

    • Cash
      • Cash on hand
        • Cash on hand – Hawaii
        • Cash on hand – Mainland
      • Bank – Checking
        • Bank #1 – Hawaii
        • Bank #1 – Mainland
        • Bank #2 – Hawaii (Payroll)
        • Bank #3 – Mainland (Payroll)

    Another scenario is integration after mergers and acquisitions. When configuring the acquired company into the ERP, you may give those account numbers similar but separate account numbers:

    • cash
      • Cash on hand
        • Cash on hand – Hawaii
        • Cash on hand – Mainland
      • Bank – Checking
        • Bank #1 – Hawaii
        • Bank #1 – Mainland
        • Bank #1 – Acquisition #1(OnoOno Chicken)
        • Bank #2 – Hawaii (Payroll)
        • Bank #3 – Mainland (Payroll)

    The best way to have all of this is to add another level to the account. You can break the account number into segments or levels. For example, I can break this into:

    • Account type (example: 1 – Asset)
      • Level 1: Major Account # (Example 010 – Cash)
        • Level 2: Sub Account (example 200 – Checking Accounts)
          • Level 3: Sub Account (example 100 – Bank # 1 )
            • Location/Region/Company – (example: 010 – Hawaii)

    I then use codes for each level to identify it, with the last level being the location.

    Account Name Type Level 1 Level 2 Level 3 Location
    Cash on Hand Hawaii 1 010 100 100 010
    Cash on Hand Mainland 1 010 100 100 020
    Cash on Hand Ono’ono Chicken 1 010 100 100 030
    Bank#1 – Checking Hawaii 1 001 010 010 001
    Bank#1 – Checking Mainland 1 001 010 010 002
    Bank#1 – Checking OnoOno Chicken 1 001 010 010 003
    Bank#2 – Checking Hawaii 1 001 010 011 001
    Bank#3 – Checking Mainland 1 001 010 012 002

    While many companies do use numeric-only systems, if you have string-based levels, you can use alphanumeric characters. I like those for the category and the location, particularly, so I can change this to.

    Account Name Type Level 1 Level 2 Level 3 Location
    Cash on Hand Hawaii A 010 100 100 001
    Cash on Hand Mainland A 010 100 100 002
    Cash on Hand Ono’ono Chicken A 010 100 100 003
    Bank#1 – Checking Hawaii A 001 010 010 HAW
    Bank#1 – Checking Mainland A 001 010 010 MLD
    Bank#1 – Checking OnoOno Chicken A 001 010 010 ONO
    Bank#2 – Checking Hawaii A 001 010 011 HAW
    Bank#3 – Checking Mainland A 001 010 012 MLD

    There’s one more thing to add to the chart of accounts: aggregate and heading categories. I’ll want to group all cash and all banking, for example, so I have this:

    Account Name Type Level 1 Level 2 Level 3 Location Level
    Assets A 000 000 000 000 000
    Cash A 010 000 000 000 001
    Cash on Hand A 010 100 000 000 002
    Cash on Hand Hawaii A 010 100 100 HAW 003
    Cash on Hand Mainland A 010 100 100 MLD 003
    Cash on Hand Ono’ono Chicken A 010 100 100 ONO 003
    Cash in Bank A 010 200 000 000 002
    Bank#1 – Checking Hawaii A 001 010 010 HAW 003
    Bank#1 – Checking Mainland A 001 010 010 MLD 003
    Bank#1 – Checking OnoOno Chicken A 001 010 010 ONO 003
    Bank#2 – Checking Hawaii A 001 010 011 HAW 003
    Bank#3 – Checking Mainland A 001 010 012 MLD 003

    That gets us to implementation, and as you can see, there’s a lot to be said for strings, since I can use both numbers and letters.

    We’ve been talking about levels, and for an identifier, we can concatenate all those strings together, so Cash On Hand could be represented as A010010001, leaving off the location. Bank#3 – Checking Mainland would be A0010101012MLD

    While I may use these for reporting, searching, and sorting, they are problematic.

    One reason people use the numerical category is sorting, so they sort in the order of assets, liabilities, etc. And I’ll change my coding system back for that reason, so Cash On Hand could be represented 1010010001, leaving off a location. Bank#3 – Checking Mainland would be 10010101012MLD .

    Consider the case of these two accounts:

    Level1 Level2 level3 code fixed code
    1 11 23 11123 001011023
    1 1 123 11123 001001123

    non-fixed, they become identical, thus impossible to track.

    So what all this gets to is some decisions:

    1. Do you use alphanumeric or numeric account numbers
    2. What parts of the number are alpha and which are numeric if using alphanumeric
    3. Do you store levels or a composite number?
    4. What does a level look like?
    5. How do you handle titles or groupings?
    6. What other information and models do you need?

    You can make many decisions on these questions. I’ll present one way, though you’ll find many, depending on the application’s developers, the platform’s limitations, and customer needs.

    In Luca ERP, within the chart of accounts, I’m going to store everything as integers. To the user, they will not appear that way. The account types and locations will come from other tables. I’ll use values in my chart of accounts table, which can serve as keys to reference the names of types and locations. So our chart of accounts table looks like this:

    Account Name Type Level 1 Level 2 Level 3 Location Level
    Assets 1 000 000 000 000 000
    Cash 1 010 000 000 000 001
    Cash on Hand 1 010 100 000 000 002
    Cash on Hand Hawaii 1 010 100 100 001 003
    Cash on Hand Mainland 1 010 100 100 002 003
    Cash on Hand Ono’ono Chicken 1 010 100 100 003 003
    Cash in Bank 1 010 200 000 000 002
    Bank#1 – Checking Hawaii 1 001 010 010 001 003
    Bank#1 – Checking Mainland 1 001 010 010 002 003
    Bank#1 – Checking OnoOno Chicken 1 001 010 010 003 003
    Bank#2 – Checking Hawaii 1 001 010 011 001 003
    Bank#3 – Checking Mainland 1 001 010 012 002 003

    Numbers are much easier to sort, so sorting is faster. Luca ERP is a small business application, so I’ll use a three-level, four-digit account code with a one-digit account type prefix and a four-digit location type suffix. This is wildly big for even most small businesses, but it gives space to grow, an important point to remember. Some systems skip that location level, but I’m going to use it for reporting later. If I were particularly sophisticated, I could make the levels variable, but I want to keep this simple.

    I want those summary lines as well. There are two ways I could signal a summary line. The simpler version, and the one I’ll use, is a flag indicating a summary row. Another option is to treat any line without levels as a summary line. There are two reasons I’m not doing it this way: Locations get very messy, and that’s more coding than I want to do for this, but it’s something you might want to explore.

    I’m going to store all the levels so I can easily search them. As integers, they are smaller to store than characters.

    SO I have a basic schema for the chart of accounts as

    @Model class ChartOfAccounts{
     var accountType: AccountType
     var level1:Int = 0
     var level2:Int = 0 
     var level3:Int = 0
     var location: Int = 0
     }
    

    Numbers can use multiplication of powers of ten to put each 3-digit code in its place on an integer. I can show the entire account number like this:

    var accountNumber: Int{
           let actType = accountType.number * 1000000000000
           let l1 = level1 * 1000000000
           let l2 = level2 * 1000000
           let l3 = level3 * 1000
           let loc = location
           return actType + l1 + l2 + l3 + loc
       }
    

    It may be useful to include strings for the account as well. I built a function to format the digits correctly,

    func fixedDigits(_ number:Int, digits:Int = 3)->String{
    	let formatter = NumberFormatter()
    	formatter.minimumIntegerDigits = digits
    	return formatter.string(NSNumber(number)) ?? "XXX"
    }
    

    Then the code is similar to the account number.

    func accountString: String{
    		let actType = fixedDigits(accountType.number,digits:1)
    		let l1 = fixedDigits(level1)
    		let l2 = fixedDigits(level2) 
    		let l3 = fixedDigits(level3) 
    		let loc = fixedDigits(location) 
    		return actType + l1 + l2 + l3 + loc
    }
    
    

    You’ll notice I used accountType of AccountType. While the decisions here could be another flexible table, I’m not going to do that – I’ll hard-wire this with an enumeration with two custom account types.

    enum AccountType:CaseIterable{
        case asset
        case liability
        case revenue
        case generalExpense
        case cogs
        case equity
        case custom1
        case custom2
        
        var number: Int{
            switch self{
            case .asset: return 1
            case .liability: return 2
            case .revenue: return 3
            case .generalExpense: return 4
            case .cogs: return 5
            case .equity: return 6
            case .custom1:return 7
            case .custom2:return 8
            }
        }
        
        var name:String{
            switch self{
            case .asset: return "Assets"
            case .liability: return "Liabilities"
            case .revenue: return "Revenue"
            case .generalExpense: return "General and Administrative Expense"
            case .cogs: return "Costs of Goods Sold"
            case .equity: return "Owner's Equity"
            case .custom1:return "Custom #1"
            case .custom2:return "Custom #2"
            }
        }
    }
    

    The account type doesn’t change much, so with two custom types, you can handle everything.

    I have the levels to help me with reporting functions later, such as the trial balance, balance sheet, and Profit and loss statements. I’ll get the appropriate detail levels for those reports if necessary.

    Speaking of levels, I’ll need some indicator accounts and summary accounts. We could look at the levels, and any level with a 0 value is blank. So, find the first 0 level, and that tells you the level.

    That’s a bit confusing. I prefer to use a level indicator.

    var level: Int
    

    If the level is less than 3, it’s a summary. If I want to make summaries for locations, I can, as I excluded them from the calculation.

    There are a few more properties we should add to the chart of accounts. One important oneis an account Name

    var accountName: String
    

    I also like to have some control over any changes, thus I’ll include a creation and modification date. I’ll talk about logs in a later newsletter, but this is the beginning of an audit trail for changes.

    var createDate:Date = Date()
    var modifyDate:Date = Date()
    

    There’s more to this audit trail, but we dont have the required infrastructure for users yet.

    Next, I’m going to add this,

    var internalId:PersistentIdentifier{self.id}
    

    Some ERPs, including SAP Business One, have an internal ID for the chart of accounts. Creating the account number and using it to find records can be computationally intensive. If we are looking for a specific record, searching by its internal ID is much faster. In SwiftData, every table has an internal ID property called id. I’m just giving that identifier a consistent name.

    Finally, we have the required properties for the ParentDataProtocol to work with our template.

    var key: Int
    var childSavedMax: Int = 0 //not used - parent only
    var isActive: Bool = true
    

    This gets interesting, as we are using manual keys and the internal key. In initialization, I’ll set the key to the account number.

    We won’t use childSavedMax, so I set it to 0 and added a comment noting it won’t be used.

    The critical one here is isActive. We never delete an account in the chart of accounts; at best, we inactivate or change them. So our delete in the user interface will use this switch to inactivate the account.

    Finally, we need one more table. I have a location number in my chart of accounts. At a minimum, I’ll need the ParentDataProtocol and a name for the location.

    @Model class LPCompanyLocation: ParentDataProtocol{
        var key: Int
        var childSavedMax: Int = 0 //not used
        var isActive: Bool = true
        var name:String
    

    As with the chart of accounts, we’ll be keeping these records forever, so isActive once again becomes our “Deletion” mechanism.

    Optionally, we can add more information:

    var street:String = ""
    var street2:String = ""
    var city:String = ""
    var region:String = ""
    var country:String = ""
    

    We can use this later and add it now to avoid migration headaches. Note that this is different from customer, vendor, and company data. It is location data within a company.

    That was a lot of work to set up the back-end of the chart of accounts. I’ll continue the chart of accounts in two weeks, along with the user interface, in an upcoming newsletter.

  • BizOneness: Production in the Sap B1 Web Client…Again

    BizOneness: Production in the Sap B1 Web Client…Again

    Back in July, I suspended my series on the SAP B1 Web client due to some frustrating deficiencies in the production module. SP2508 was supposed to fix some of these. Unfortunately, SAP fixed none of them.

    Instead of suspending this for another few months until more bugs are fixed, I’m going to review those issues and use this as an opportunity to explore something I usually do in the LucaERP newsletter: describe why companies and developers make decisions when building an ERP. I don’t know SAP’s reasoning, but I can give you some ideas about what I’m thinking about writing these modules.

    The Bug of Materials

    We start with a bug in the Bill of Materials (BoMs). Entering the selection page for BoMs, it stares you in the face, though you might miss it.

    For other such pages, we have entries from the parent or father tables. I discussed the parent-child relationship of one-to-many tables last time in the bizoneness newsletter. The parent table here makes sense: it is the one connection to all the line items associated with it. In the Bill of Materials, the preant child relationship is OITT for the Bill of Materials parent and ITT1 for the items in the Bill of Materials, such as inventory items, subassemblies, routing, and more.

    However, the bill of materials section shows ITT1, the children’s line items, not the parent. That makes a lot more repetitive items to pick through.

    Since I can’t think of any good reason to do this, this is a bug. Most likely, this is the case of the developer not knowing their tables or making a simple typo. It’s the kind of thing I expect from Vibe coding through AI, which lacks sufficient context of the database tables.

    What irritates me is that it is such a glaring bug, breaking user expectations of product functionality, and it still isn’t fixed.

    You can click on a row to get the correct bill of materials, including both parent and child information.

    I can edit and create new BoMs, and the web client has buttons to filter by Route stages, Items, resources, and texts.

    While earlier versions did not let you save a BoB, the create and edit buttons are now available. I didn’t try to save a new BoM; I was afraid of what I would find.

    Production Orders.

    I’ll then go over to the production orders. What you find is disturbing – this is read-only.

    The selection page works correctly, giving us the parent table to access the rows.

    When you select a production order, you have no way to edit or add more production orders. The function bar is gone. This, of course, means you cannot release the order to production or close a production order from the web client.

    Any method you think of for adding or editing a production order doesn’t exist.

    This is more than a bug; it is either a feature or a missing feature. The question becomes why it is missing.

    The simplest answer is that production is not a high priority for SAP. Production is not a “sexy module.” Not everyone uses it. Those involved in the service industry won’t, and many in the distribution or Retail do not need it. That brings it down towards the bottom of the TODO list compared to Finance, or Sales, or Dashboards.

    The standard SAP B1 Client can attest to this possibility as well. Coming from a shop that did both mass manufacturing and custom jobs, I’m well aware of the deficiencies. The number of features missing from SAP B1 is quite high, including fixed cost calculations, job costing, production variance, quality measures, etc. Some features, like routing and resources, are tacked on as extras.

    Another possibility is that the production orders module in the web client is intentionally read-only, requiring any changes to be made on the RDP client only. There may be ways to share user IDs on the web client that are not possible in the RDP client. If there is any case for sharing user IDs, it’s most likely in production, especially in a small business environment that can’t afford many licenses.

    However, the bug in the bill of materials leads me to choose option 3 in this case as well: the code is poorly written. Whether this is due to AI vibe coding, bad programmers, or bad architecture is something I cannot know for certain. My biggest clue is the lack of context in the action bar. Every module, even those that have no use for it, such as the bill of materials, has an “Add activity” button. That points to bad architecture. An error like that would not be so consistent across the system.

    Sadly, the production module is worthless in the SAP B1 web client. If it worked, I could see a lot of use for it based on my background in medical device manufacturing. Production is ideal for use on tablets running web browsers instead of desktops. I spent four years waiting for a fixed costs feature in SAP B1, only to be told it was in the next build. From that experience, I’m skeptical about whether a functional production module will show up in SAP.

    I’ll revisit this once the module actually works. In the meantime, for my next column, I’ll continue to find what works in the Web client.

  • LucaP_ERP14 Setting up the Hybrid Template for Persistence.

    LucaP_ERP14 Setting up the Hybrid Template for Persistence.

    I’m building an ERP system from scratch to explain all the issues involved in developing an ERP. We’ve looked at user interfaces and have been working on content for a few newsletters. It’s time to assemble everything and make the hybrid template we’ll use the most in LucaERP modules.

    As I’m mostly an Apple developer, I use SwiftData for persistence. If you want to know more about SwiftData, check out my course, SwiftData Essential Training. I’ll be jumping around more to explain issues with persistence in a programming language rather than focusing on teaching SwiftData here.

    We’ve made some big changes to the template, which I built into a CRUDableReport-Demo application. We have to change the model, and then the CRUD functions (Create, Read, Update, Delete). To summarize these changes:

    1. Change the model from an array to a persistent model.
    2. Add the fetch method to read the parent data (read)
    3. Change navigation for parent data (read)
    4. Change modification (update)
    5. Change delete (delete)
    6. Change add (create)
    7. Add the fetch method to read the child data (read)
    8. Change navigation for child data (read)
    9. change modification (update)
    10. change delete (delete)
    11. change add (create)

    Which is a lot. I’m going to summarize these changes instead of giving details. Most of that is covered in the SwiftData Essential Training course on LinkedIn Learning.

    Unfortunately, for this version, I got rid of much of the modularity of waht we’ve been working with. The problem is that a Swiftdata variable modelContext is communicating with the tables. Passing it is not easy, so everything that uses it in this iteration will be added back to my view. So the protocol Crudable and much of the report view merge into the current template. I want to get this working more than having a good structure. If I can get it working and debugged, I’ll move to making it easier to modify the template if necessary.

    Models

    As discussed in the last newsletter, I’m using two separate tables for our parent and detail views to better parallel external databases. Making the detail table an array inside the parent is much more efficient, but I want to keep things on a more common basis for educational purposes.

    I will also call the tables for one-to-many relationships: parent-to-child, where the parent represents one row and the child or detail represents the multiple associated rows. For example, a sales order includes the sales order number, customer name, and address. This is the parent, but the items ordered are children.

    Originally, I used a struct added to a class with an array to make a model. That all changed with Swiftdata. I made that struct a class and added the @Model macro. The original class disappeared. Here’s an example.

    @Model
    class ParentModel: Activatable{
        var parentID:Int
        var isActive: Bool
        var name: String
        var lastChildID: Int
        init(parentID:Int, name:String,isActive:Bool = true){
            self.parentID = parentID
            self.isActive = isActive
            self.name = name
            self.lastChildID = 0  //stores the detail id info
        }
    }
    

    @Models are by nature Identifiable and Equatable, so I don’t need the extra protocols; they are just Activatable. Identifiable should have a property id, yet it is missing. It’s buried inside the model’s PersistentIdentifier type, which itself uses a type called UUID, essentially a very large string composed of random numbers. UUIDs aren’t great for searches, so for our access purposes, I added a parentID to do what id did before.

    For the child, I have two keys: a parentID and a row. I use the parentID to find the children associated with parentID, and I use row to differentiate between the children.

    Storing keys

    In the past, I used a method to keep keys unique by finding the maximum value for the key and then adding 1 to it for the next key. This has two drawbacks: in larger databases, it requires extensive computation to find the key, involving many file accesses or large memory storage.

    Secondly, there’s a bug we’ve ignored until now. I explained this in detail in another post, but here’s a summary. Suppose you had the keys 1, 2, 3, 4, 5 as parents in a one-to-many relationship. You delete 5, but you dont delete the children to 5. When you add another row, it will be 5, pointing to the zombie children that the original 5 had, which isn’t what you want. This could be a deleted invoice 5, that has ghost items from the original 5, for example. The best case is never to use 5 again.

    If the child table were an array within the parent table, this would not be a problem, as it would do a cascade deletion, removing the child records along with the parent. In some situations, we want to keep these independent tables.

    The solution requires persistence. Store the last key in a table or file that you can access to get the next key. Get the previous key, which in our example is 5, increment it, and use it for the newest row, storing the latest value, 6, back in the model. It requires fewer fetches of records, and there is less chance of problems. For one-to-many relationships, you can store that number for children in the parent as a column, such as lastChildRow.

    For the parent table, I have a different arrangement. I use a new table, I’ll call it KeyTable, which keeps track of this for all modules. I’ll use a string with the name of the table as my key.

    @Model
    class KeyModel{
        var moduleID:String //Primary Key
        var key:Int
        init(moduleID:String, key:Int = 0, doc:Int = 0){
            self.moduleID = moduleID
            self.key = key
        }
    }
    

    This setup has another advantage: not all systems will have a document that starts at zero, such as a conversion from one system to another, and one must maintain continuity of documents, such as A/R invoices and Sales orders. We can go into a table in the ERP and change those numbers to be where we want them to start.

    In a recent article I wrote, many people made a false assumption concerning keys: that the number must be in sequence. In this example, we have valid keys of 1,2,3,4,6, for instance. We care about uniqueness, not sequential numbers. There will be skipped numbers, especially in multi-user environments with two instances modifying tables. I could start and cancel one sales order and complete another started a second after the first. The number for the first sales order is lost, but we maintain uniqueness.

    Fetching the Parent Model

    Having the models set, we fetch the parent model and the dependency of the key storage.

    In earlier versions of the temple, I used blank. I decided, for several reasons, that changing blank to nil was a better idea. Most notably, Swift optionals give me errors that I can track much more easily than blank does.

    I’ll load the entire table using a function fetchParent() to return the table. I’ll be using this in a few places to update the table. The first is when the view appears, I load the data.

    The second table I’ll fetch is the key table, creating a fetchKey method. There are three new declarations I need to make:

    let modelName = "Parent"
    @State var keys:[SavedKeys] = []
    @State var selectedKey: SavedKeys! = nil
    

    We need only one row from the KeyModel table, and that row is always the same, so we have a constant modelName to identify the table we’re using. In the fetchKey method, I load the appropriate key and save it as a value, as long as it exists. If it doesn’t, I make the key and save it back to the table.

    Adding Parent and rows

    Parent rows use the new primary key setup. I’ll use the nextID method to increment the selectedKey’s value and update the selectedKey. Due to the way SwiftData updates tables, this immediately saves the new key value in the table. The code simplifies to making a new row and adding that row to the Parent table.

    Child rows do the same thing as the parent, but using data from the parent instead of a separate key.

    Cascade deletes and invalidations

    One feature to add, based on the bug we’ve already talked about, is a belt and suspenders approach to deletion and inactivation. If a parent is deleted or inactivated, so are its children. This requires a method for deleting the children.

    private func deleteChild(for parent:Parent){
            let deletedChildren = children.filter{$0.parentID == parent.parentID}
            for child in deletedChildren{
                modelContext.delete(child)
            }
        }
        
        private func inactivateChild(for parent:Parent){
            let inactivatedChildren = children.filter{$0.parentID == parent.parentID}
            for child in inactivatedChildren{
                child.isActive = false
            }
        }
    

    Then code for checking if a cascade is required in the parent deletion:

    private func inactivateParent(_ parent: ParentModel, cascade:Bool = true){
            withAnimation {
                if cascade{
                    inactivateChild(for: parent)
                }
                parent.isActive = false
            }
        }
        private func deleteParent(_ parent: ParentModel, cascade:Bool = true) -> ErrorType {
            withAnimation {
                if cascade{
                    deleteChild(for: parent)
                }
                modelContext.delete(parent)
                if cascade{
                    fetchChildren()
                }
            }
            return .noError
        }
    

    The rest of the code changes slightly for the implementation of persistent data. It is why I started with arrays to make this an easier change.

    I’m still debugging the code, so I’m not giving any code out this time. In two weeks, I plan to have debugging done and get to what I originally intended for this column – discussing the problems of building the ERP, not its infrastructure that I have been playing with for over six months. We’ll start with the chart of accounts and general journal again and get them into the templates.

  • BizOneness The Primary Keys DocNum and DocEntry

    BizOneness The Primary Keys DocNum and DocEntry

    In SAP, we use two columns regularly: DocNum and DocEntry. But do you know what they actually mean, where they come from, and why you would want to use one over the other? What do they have to do with the ValidFor column? Let’s deep dive into DocNum and DocEntry to explore their many uses and strategies for use.

    Much of what I’ll discuss here is being worked out in code on the LucaERP biweekly newsletter. I’ll be discussing how to code this next week for LucaERP. For this newlettter, I want to address those who create effective reports or the occasional user-defined table. You’ll find some insights about good design in the SAP Business One world, and some things that might trip you up if you are not careful.

    Let’s assume you have some data in the Sales Order table ORDR. Why is one sales order different from another? It’s not the total – two sales orders for $1,000 are impossible to distinguish with that much information. You could use a set of criteria, such as the customer ID, sales date, and amount, to access it; however, that would be a lot of work. Instead, we use the sales order number.

    The sales order number has several characteristics that are true of it. First, it always has a value. Unlike other columns, which can be blank or, in SQL terms, NULL, the sales order number always has a value in a simple type, most often an integer or a varchar. It will also be unique. No other row has that value for this column.

    In SAP Business One, values like this, called keys, can be found in two columns: DocEntry and DocNum.On a sales order, users refer to the DocNum, the number found on a sales order. docEntry is hidden from you, even in the Query generator when making reports. Why two?

    Much of the answer deals with keys having unique values and how to generate those unique values. There are several. In the Business Partners module, the customer number is also a key, in this case, OCRD.CardCode. The user adds card codes manually. If a code already exists, you get an error.

    Some systems do this automatically. If numbers are involved, a common method is to find the highest value for the key and then add one. I did this in LucaERP. If you have automatic key generation in Business Partners, that is how it works: either as a user-defined value or through a macro in an add-on like Boyum B1UP.

    There is a bug with this method, however. Suppose sales orders use this method, and you have sales orders 1, 2, 3, 4, and 5. You delete sales order 2, and that works out without a problem. Suppose you deleted sales order 5, which had $200 worth of goods. The next order you put in will be $200 bigger than expected.

    The culprit is the RDR1 Table. If you delete the original sales order 5, you don’t delete the RDR1 table rows for it. When you add the next sales order, it sees 4 as the maximum, adds one to get 5, and when it calculates the rows, it includes the rows for the original order 5.

    There are several things you can do about this:

    One is for the system to do a cascade delete. When it deletes the ORDR item 5, it delete the associated records in RDR1. If there are records under RDR1, we could delete them too. However, there are cases where I dont want to delete. For example, I don’t want to delete an inventory item because it’s in previous sales orders and invoices. This is the reson the system does the deletion and not users or administrators. THe SAP B1 licence prohibits manual deletes to prevent this and othere mishaps.

    Another is to inactivate instead of delete records. Inactivating is a vary common and preferred method for auditing reasons in SAP B1. It allows us to easily identify what was inactive and also addresses the bug where other tables reference a value. For example, inactivating an inventory item leaves it as a row in the Item master data, but marks it as not to be used. Inactivation uses some flag, which in SAP B1 is usually the column validFor. There are options in SAP Business One for timed activation and inactivation of some records, such as seasonal sales items. However, for our purposes, the inactivation keeps ORDR item 5 in place, and while five does not appear in sales orders, the next new sales item is 6.

    The one used by sales orders, however, does not use increments on maximums. Instead, it stores the last value used and increments it. Even if we delete that previous value, the value increments from this stored value. In SAP, this is set up in Administration>Document Numbering. Since you need to be careful with key assignments, I’m not giving you tables for this. What I will tell you is to have high security to make sure your documents do not get messed up.

    There’s another use for these stored document numbers: conversions and migrations. If a legacy system has a series of numbers, set the document numbers above the last number in the legacy system to prevent duplicates.

    All of this usually happens with DocNum. This brings us back to DocEntry. SAP uses DocEntry as an internal ID starting at 0. If you started both DocNum and DocEntry at 0, they will remain the same. If you start DocNum at 1000, and DocEntry starts at 0, they will never be the same.

    SAP uses DocEntry as an internal ID for all relationships between tables, especially marketing documents. For example, ORDR.DocEntry matches up to RDR1.DocEntry for the line items. This is how we do JOINS between one-to-many relationships.

    SELECT T0.DocNum,T0.DocEntry,T1.LineNum,T1.LineTotal
    FROM ORDR T0
    INNER JOIN RDR1 T1 ON T0.DocEntry = T1.DocEntry
    

    When using the Query generator, you’ll notice ORDR will not list DocEntry as a column. I’m not a big fan of this, but SAP does have a reason: it’s supposed to be invisible to the user. If you add RDR1 as a table to the query, the join will automatically place

    INNER JOIN RDR1 T1 ON T1.[DocEntry] = T0.[DocEntry]
    

    in the query.

    SAP is right for most users. A hidden DocEntry avoids confusion. For those who write queries and Crystal Reports, it is useful for testing. Many queries and forms, such as A/R invoices, will run off of DocEntry, but show DocNum on the document. Debugging is impossible for many forms, because if you test with DocNum on a Crystal Report that selects by a DocEntry, you are looking for the wrong row, as I’ve done many times.

    I include the DocEntry in my queries and any data fetching for Crystal Reports. When I put an SQL Report into production, I’ll delete the DocEntry. For Crystal Reports, I’ll leave the entry on the form and hide it in production, making it easy to show again if I need to work on the form.

    Keys drive relational databases that are the core of any ERP system. SAP B1 has conventions to keep most one-to-many relationships in these databases with the DocNum for user identification and DocEntry for the internal identification.

  • LucaP ERP 12: Transitioning for Persistence with SwiftData

    LucaP ERP 12: Transitioning for Persistence with SwiftData

    I’ve been writing an ERP from scratch for a few months and have gotten some of the foundations down before we get into coding modules. The three templates for interacting with data are most important: a form, a report, and a hybrid. The hybrid template became a form that could optionally contain a report, and that’s what I will be using moving forward for all three. But it doesn’t remember anything – we need to connect it to persistent memory, a way of storing our data between uses. Most of the time, that’s a database system such as SQL. As I’m writing in Swift, that’s SwiftData. Our next step is to convert our current application to SwiftData.

    Over the next few newsletters, I’ll be doing that. Unlike the previous newsletter, I will not go deep into code but explain broad concepts. There are two reasons for this: not everyone uses SwiftData, and I want to explain for many persistent systems besides SwiftData. The second is that you can support the work I do here and watch my LinkedIn Learning course SwiftUI Essential training, where in 3.3 hours you can learn all the details, and I can get some royalties.

    So, let’s examine SwiftData in general and then discuss the issues of transforming Luca ERP into a persistent application.

    What is Persistence

    Now, I’m using the word persistence here for a reason. Persistence is any data stored between application runs. Storing data in a text, XML, or JSON file, as well as full relational databases, is persistence.

    SwiftData is Apple’s latest attempt to work with persistent data. It is a new layer of the onion of technologies Apple has been using for decades. SwiftData is built to work well with SwiftUI. It was built on an older technology, CoreData, which can use different forms of persistence depending on the job. For database operations, both CoreData and SwiftData rely on SQLite, a very popular open-source framework written in C incorporating single-user SQL into applications.

    SQL Equivalents

    Because SQLite is near the core, you’ll notice SwiftData patterns that are similar to SQL. If you have any SQL background, this should look familiar:

    SELECT ... FROM ... WHERE ... ORDER BY
    

    SwiftData has similar structures. Like SQL, you build tables based on a row structure. In an object-oriented programming language, these are arrays of a class containing your columns. You then query this table using a fetch descriptor made of two parts.

    The first is a predicate. I’ve discussed predicates in a previous column. They were like the WHERE clause of SQL SELECT statements there. In SwiftData, they add a single FROM. In SQL, if I want to find all the items in a SAP B1 inventory for sale, I’d use this:

    FROM OITM WHERE sellItem = `Y`
    

    In SwiftData, I’d have an object assigned to a constant:

    let predicate = #Predicate{$0.sellItem == true}
    

    For sorting, I’d do something similar, which in SQL would be

    FROM OITM ORDER BY itemCode
    

    becomes in SwiftData:

    let orderBy = SortDescriptor(\OITM.itemCode)
    

    The difference is I’d put the fetch and sort descriptors together in a fetch descriptor, and then either manually fetch the records:

    let fetchDescriptor = FetchDescriptor(predicate: predicate, sortBy: [orderBy])
    if let rows = try? modelContext.fetch(fetchDescriptor){
    	childTable = rows
    } else {
    	childTable = []
    }
    

    or load automatically

    @Query(filter:predicate,sort:[orderBy]) var rows:[OITM]
    

    nils and nulls

    When using Swiftdata, we get to something I’ve been trying to avoid in this series. You might have heard an ancient Zen riddle: What is the sound of one hand clapping? In databases and application development, we have an answer to an equivalent question: What is the value when there is no value? That value is nil or null or some similar name.

    Let’s look at a database example for this. You have an existing table of recipes and want to add another mandatory column for ratings. All new recipes will get a rating, but what happens to all the old ones when you add that rating? They have a value of NULL.

    I faked this earlier by using -1 for the ID, for example, a value we can’t use anywhere. However, nil and null are easy to test for. In Swift, for example, the code we looked at earlier is handling nil.

    if let rows = try? modelContext.fetch(fetchDescriptor){
    	table = rows
    } else {
    	table = []
    }
    

    It is a compact version of an expanded code:

    let rows = try? modelContext.fetch(fetchDescriptor)
    if rows != nil {
    	table = rows
    } else {
    	table = []
    }
    

    The try? returns nil when an error occurs during a table’s fetch; otherwise, it returns the table. I then assign the rows to my property and, if nil, make an empty table.

    In Swift, values that can be nil are called optional values and must be declared as such. Optional values do not play nice with non-optional values, causing a fatal error when you assign a value of nil to a non-optional value. table above is not optional, and so some form of if statement is usually necessary to handle the nil.

    Especially in SwiftUI for optional values, you may also see the nil coalescing operator ??, which provides the inline evaluation necessary. With the ?? operator I could rewrite the code to this:

    let rows = try? modelContext.fetch(fetchDescriptor)
    table = rows ?? []
    

    When rows is nil, assign an empty array to table.

    The downside of optionals is the extra code necessary to handle the nil cases. Optionals, however, are unavoidable in persistence, so we’ll adopt our code to use them.

    Replacing CRUDable

    The biggest change is to the Crudable protocol. The point of the protocol was to do many, though not all, of the functions that SwiftData does for us. The basic CRUD operations are replaced with methods from the ModelContext object of SwiftData, such as inserting, fetching, and deleting. Update works the same: when changing the array, you change the stored data. All of this will be in my view and not the protocol or its extension this time. I also changed my mind on how I’ll handle errors, as many of the methods using a do ... try ... catch will return more robust error messages than I can generate.

    Navigation also changes in something I should have planned for. SwiftData has its internal value ID, different from our keys. However, I’m still going to base everyhting on the keys. Here too, nil will simplify the work.

    Codable, which was based on arrays, provides much of the framework for what we will add to this template. We were able to build much of our user interface without having to deal with bug from back end problems of persistent memory.

    Prototyping and play

    Now I’ll put all these changes into code. One of my favorite stories from Jim Collins’ book Built to Last is the one about the pirates. Imagine you are a pirate ship in a fog. Somewhere in that fog is a treasure ship ready for the taking with one cannonball shot. You have a few cannonballs but lots of musket rounds. Firing a musket is cheap, but firing a cannon is expensive. You fire muskets into the fog, and listen if you hit anything. Where you hear a hit is where to fire your cannon.

    This scenario is better described as play or prototyping. Make numerous small, inexpensive investments in engineering and development, and only pursue the big ideas if one of them seems successful. This is my development cycle, with the added knowledge that what I made before are components of the new idea I’m playing with.

    I’ve been doing that for most of this series. I’ll build something small then try something else, testing for success as I do. I could create a single large project, but testing for bugs becomes challenging due to issues with the user interface, data structures, or persistent data. So I tend to build the UI first with much more testable data and then move to persistence.

    I’ll take all my ideas and learnings and put them together in a more elaborate prototype. Eventually, I’ll have a workable building block for the rest of my application.

    With everything we’ve discussed, I’ll put together from scratch the next iteration of the form template, this time with persistence. In our next newsletter, we’ll talk about joining tables together in SwiftData. There are several options to choose from depending on what you want to do with your data. Once we select an option, we’ll discuss creating the final report and hybrid template.

  • LUCAP_ERP11 The Hybrid Template with Predicates

    LUCAP_ERP11 The Hybrid Template with Predicates

    We’re building an ERP from scratch and are currently working on three templates to do all the work. We’ve completed the form and the reporting view. Invoices, bills of materials, and other modules require a template that has three components: a form at the top called a header, a report in the middle known as the body, which contains details, and a form at the bottom called a footer. This time, we’ll combine the two templates we have to create the third. Before we do, there’s one improvement we’ll make to the report: Adding predicates.

    Predicates

    A predicate is a Boolean expression that we use to filter our data. If true, it is valid data. Why you need them is easily seen in SQL. There are two places you’ll commonly see a predicate: The WHERE clause and the ON clause.

    SELECT 
    	t0.id,
    	t0.name,
    	t0.address, 
    	t1.item,
    	t1.quantity
    FROM 
    	Order T0,
    	INNER JOIN OrderRow T1 ON t0.id = t1.parentId
    WHERE
    	t0.id = 123456
    

    We’ve already looked at the predicate of t0.id = '123456' that’s the R of CRUD, filtering for one record. In Swift, we wrote that as

    row = table.first{$0.id == 123456}
    

    Notice the braces that place the boolean expression in a closure. The $0 is shorthand. Expanded out, we write this:

    row = table.first{row in row.id == 123456}
    

    Let’s look at the ON and what it does. It matches all cases where a child table has an id that is the same as the parentID. Graphically this is

    insert image here

    In Swift, instead of using first, as we did for our WHERE clause, we would use the filter method, which again takes a boolean closure for a predicate.

    rows = childTable.filter{$0.id == parentID}
    

    This expression returns all rows relevant to the parent. While there are cases where reports require every row, most will be filtered to include only relevant information. A daily sales report will show only today’s or yesterday’s sales. They might be filtered to only sales bigger than $10,000. Alternatively, the report might be part of the hybrid report and give only child rows for the parent.

    For this to be flexible enough to work, we’ll utilize a special feature of closures and their counterparts in other languages, lambdas and blocks: they can be assigned to variables and passed as arguments.

    I can do this in Swift:

    var table:[Int] = [3,1,4,1,5,9,2,6,5,3,5,8,9,7]
    var even:(Int)->Bool = {item in item % 2 == 0} \\<--- Predicate
    var evens = table.filter(even)
    print(evens)
    

    This code returns only the even numbers. I declared my closure as

    (type)-> Bool 
    

    the same way I declare a Int or String. If this looks familiar, it is: a func is a form of a closure.

    func even(item:Int)->Bool{return item % 2 == 0}
    

    To take this one more mind-blowing step, the function can be a function of predicates:

     static func evenOdd(isEven:Bool = true) -> (Test)->Bool {
            if isEven{
                return {$0.a % 2 == 0}
            }
            return {$0.a % 2 != 0}
        }
    

    In our hybrid template, we’ll be filtering for the child view. If you recall several installments ago, we discussed child data, which typically includes the details of a parent data point. For example, a pizza recipe will have parent data about the pizza, and child records with the ingredients used.

    When we change rows on the parent, we show the corresponding rows of the child. Passing a predicate makes it much easier to set the correct data.

    Updating the report template for hybrid

    In the last newsletter, I ran into a problem with ForEach loops and spent a column explaining them. I decided to start from scratch, writing the report instead of fixing all the errors that came with the existing report structure.

    I’ll make a new ReportUIView. Most, but not all, of what I’m doing was discussed in earlier newsletters as we built the report and form templates.

    However, there are some significant changes, so I will focus on those.

    The New Model: David’s Grindz

    Up to now, for the templates, we had a flat model. This time, we need a parent-child relationship, a one-to-many setup. Taking a cue from the capstone project David’s Grindz in my LinkedIn Learning course The Complete Guide to SwiftUI, I used a variation of a pizza recipe file.

    First, there’s the parent, which adopted Crudable. Remember, Crudable is the protocol I used to handle most of my file functions.

    struct TestModelRow: Identifiable, Activatable, Equatable{
        var id: Int
        var isActive: Bool = true
        var name: String
    }
    
    
    class PizzaModel: Crudable{
        typealias Row = TestModelRow
        var table:[Row] = [
            TestModelRow(id: 0,isActive:true, name: "Huli Pizza"),
            TestModelRow(id: 1,isActive:true, name: "Longboard"),
            TestModelRow(id: 2,isActive:true, name: "Pepperoni Pizza"),
            TestModelRow(id: 3,isActive:true, name: "Margherita")
        ]
        var blank: Row{
            TestModelRow(id: -1, isActive: false, name: "")
        }
        
        var nextID: Int{
            (table.map{$0.id}.max{$0<$1} ?? -1 ) + 1
        }
        
    }
    

    For the child, there’s one difference in the rows. I added a parentID to link back to the parent.

    struct IngredientModelRow: Identifiable, Activatable, Equatable{
        var id: Int
        var parentID:Int
        var isActive: Bool = true
        var name: String
    }
    

    I’m keeping this simple for now. I could create another column for the ingredient row and then build unique IDs from that. For now, I won’t do that.

    I created an IngredientModel class that inherits from Crudable and includes some sample data.

    class IngredientModel: Crudable{
        typealias Row = IngredientModelRow
        var table:[Row] = [
            // With huli huli chicken, onions, ginger, crushed macadamia nuts, tomato sauce, and cheese on a classic crust.
            IngredientModelRow(id: 0, parentID: 0, name: "Hawaiian crust"),
            IngredientModelRow(id: 1, parentID: 0, name: "huli huli Chicken"),
            IngredientModelRow(id: 2, parentID: 0, name: "onions"),
            IngredientModelRow(id: 3, parentID: 0, name: "fresh ginger"),
            IngredientModelRow(id: 4, parentID: 0, name: "mac nuts"),
            IngredientModelRow(id: 5, parentID: 0, name: "tomato sauce"),
            IngredientModelRow(id: 6, parentID: 0, name: "firm mozzarella"),
            IngredientModelRow(id: 7, parentID: 0, name: "Huli Sauce"),
            //A very long flatbread for vegetarians and vegans, made with olive oil, mushrooms, garlic, fresh ginger, and macadamias, sweetened with lilikoi.
            IngredientModelRow(id: 8, parentID: 1, name: "classic rust"),
            IngredientModelRow(id: 9, parentID: 1, name: "olive oil"),
            IngredientModelRow(id: 10, parentID: 1, name: "mushrooms"),
            IngredientModelRow(id: 11, parentID: 1, name: "Garlic"),
            IngredientModelRow(id: 12, parentID: 1, name: "fresh ginger"),
            IngredientModelRow(id: 13, parentID: 1, name: "mac nuts"),
            IngredientModelRow(id: 14, parentID: 1, name: "lilikoi sauce"),
            //The New York Classic version. A thin crust with pizza sauce, cheese, and pepperoni
            IngredientModelRow(id: 15, parentID: 2, name: "New York crust"),
            IngredientModelRow(id: 16, parentID: 2, name: "firm mozzarella"),
            IngredientModelRow(id: 17, parentID: 2, name: "pepperoni"),
            IngredientModelRow(id: 18, parentID: 2, name: "pizza sauce"),
            //The classic pizza of Buffalo Mozzarella, tomatoes, and basil on a classic crust.
            IngredientModelRow(id: 19, parentID: 3, name: "classic crust"),
            IngredientModelRow(id: 20, parentID: 3, name: "tomatoes"),
            IngredientModelRow(id: 21, parentID: 3, name: "fresh basil leaves"),
            IngredientModelRow(id: 22, parentID: 3, name: "Buffalo mozzarella"),
        ]
        
        var blank: Row{
            Row(id: -1,parentID: -1, isActive: false, name: "")
        }
        
        var nextID: Int{
            (table.map{$0.id}.max{$0<$1} ?? -1 ) + 1
        }
    }
    
    

    Calling Actions

    Reports do not change rows the same way we select forms with navigation buttons. Instead, we click on the row to select it. These toolbars and the okay button in the bottom toolbar used a set of variables, displayMode, navigationAction, and doAction, to work.

    For our new version of the report, I can directly use the buttons I need, which are very limited in comparison. This makes for much cleaner code. For example, to add a row, I do this:

    Button{
        let newId = childModel.nextID
        let newRow = Row(id: newId, parentID: parentID, isActive: true, name: "")
        performAction(actionMode: .add, newRow: newRow)
    } label:{
        Image(systemName: "plus")
    }
    

    I’m still using the performAction method from before, but a pared-down version. Instead of automatically setting a new row, I set it in the button’s action. I save myself a lot of monitoring for changes.

    I decide which action to take with the selectedChildRow state variable. My displayed rows are in a label. A tap on a row selects the row, showing it as yellow.

    Button{
        selectedChildRow = row
    }
    label :{
    //Display the row here-----
    Hstack{
    ...
    }
    	.foregroundStyle(.black.opacity(row.isActive ? 1.0 : 0.5))
    	.background(.yellow.opacity(selectedChildRow.id == row.id ? 1.0 : 0.01))
    } 
    

    There’s also a dimming feature for inactive rows when displayed. Rows appear black when active, instead of the usual button tint color.

    The Loop

    Encapsulating this button is the ForEach, iterating over the rows:

    ForEach($childModel.table.filter({$row in row.parentID == parentID && (row.isActive || showInactive) })){$row in
    ...
    }
    

    There is a lot to unpack here. because I’m using binding varables in the Textfields, this has to be binding, thus the $childModel, and the $row in

    In between all that is the predicate, and it is a big one, accomplishing two tasks.

    {
    $row in row.parentID == parentID 
    && 
    (row.isActive || showInactive) 
    }
    

    The first half finds all rows that match the parent ID sent to us from the superview. I defined parentID as

    @Binding var parentID:Int
    

    The resulting collection consists only of ingredients from that recipe.

    The second part has a different use. I want to hide or dim any inactive rows. Inactivating rows is something we need to consider in accounting. We cannot delete a record in many circumstances due to auditing reasons, but we usually would like it to disappear from view. There are cases where seeing inactive records is useful, so the row has a flag isActive, and there is a second flag showInactive. If either of these is true, then the row displays. As we’ve seen, inactive rows will be at 50% opacity compared to active rows.

    Layout problems

    I also realized that we have some problems with the layout. In the last report template, I changed the modifiers for the input fields to include a fieldWidthand then used a formula to control that, with 25% allocated to the label and 75% to the field. However, I’ve realized making this completely manual is a better strategy.

    The totalFieldWidth variable becomes two variables:

    //var totalFieldWidth: Double
    var labelWidth: Double
    var fieldWidth: Double
    

    However, I didn’t want to refactor all the code that currently uses totalFieldWidth right now. I took an approach of two initializers, one for the legacy one and one for the new version.

     //original with a twist. Used to be the default, but made for adding the modification below.
        init(label:String,value:String,isActive:Bool = true, totalFieldWidth:Double){
            self.label = label
            self.value = value
            self.isActive = isActive
            self.labelWidth = totalFieldWidth * (label.isEmpty ? 0 : 0.25)
            self.fieldWidth = totalFieldWidth * (label.isEmpty ? 1 : 0.75)
        }
        
        
    //modification from original. This keeps columns for both labels and fields to be specified.
        init(label:String,value:String,isActive:Bool = true,labelWidth:Double,fieldWidth:Double){
            self.label = label
            self.value = value
            self.isActive = isActive
            self.labelWidth = labelWidth
            self.fieldWidth = fieldWidth
        }
    

    Legacy code still works, and the more specific code will set up correctly.

    The new fields appear as follows in the ReportUIView.

    //Show independent of selection, usually read-only columns
    LPIntField(label: "", value: $row.parentID, isActive: false, labelWidth: 0, fieldWidth: 50)
    LPIntField(label: "", value: $row.id, isActive: false, labelWidth: 0, fieldWidth: 50)
    

    Active and Inactive

    Note that these two rows have isActive set to false. We don’t want these two columns changed by the user. Only the name field is allowed to change.

    Here you may see one of two strategies. Some like the strategy of having all fields open to change. This works well and makes changes easier for the user. It does take more memory to do so. I prefer only changing the selected row. The user selects the row, and then the chosen row allows interaction.

    For that, I use an if to check if the row is selected:

     // Show when row is selected ---------------------------
    if isSelectedRow(row){
    	LPTextField(label: "", contents: $ingredient, isActive: row.isActive, labelWidth: 0, fieldWidth: 200)             
    } else {
    // display when not selected
    	Text(row.name).frame(width:200)
    }
    

    Deletion

    To delete, we use a menu attached to the end of the row. Again, I’m leaving both the option of deleting and inactivating. I’ll choose one over the other depending on circumstances.

    Menu {
    //Deletion -----------------------------
    	Button{
        performAction(actionMode: .delete, newRow: selectedChildRow)
    	} label:{
        Label("Delete", systemImage: "trash")
    }
    //Inactivation -------------------------
    	Button{
    		toggleActive()
    	} label:{
        Label(isActive ? "Inactivate" : "Activate", systemImage: "sleep")
    	}
    } label: {
        Image(systemName:"ellipsis.circle")
    }
    

    Updating

    For updates, we take care of that when we select a different record. AS long as this is a valid record, update the record in the table, then fill the views for input with the new selected data.

    //Update the row when the selection changes-----------------------------------------------
            .onChange(of: selectedChildRow.id) { oldValue, newValue in
                if  oldValue >= 0{
                    let newRow = Row(id: oldValue, parentID: parentID, isActive: isActive, name: ingredient)
                    performAction(actionMode: .update, newRow: newRow)
                    ingredient = selectedChildRow.name
                }
                ingredient = selectedChildRow.name
                isActive = selectedChildRow.isActive
            }
        }
    

    Scroll Views

    The last major problem is scroll views. There are two different ways of setting them up in this code. The first option is to scroll through the report only, and the second is to scroll through the full form. The choice you make often depends on the device. If you are working on a desktop with ample screen real estate, scrolling through rows makes sense. On the other hand, tablets, especially ones that will take up half of the screen for an on-screen keyboard, will want to use a scrolling view for the entire form.

    I prefer the scrolling form. I’ll add a vertical scroll view to the entire form. Some people might nest these, but I like the one scroll.

    I will, however, add a horizontal scroll view to the report view. This way, if there are more columns than screen real estate, you can scroll over to see more columns.

    We’ve come a far way and now have our templates. There’s more to do here and a few bugs, but we’ll address those next time, when we add persistence to the templates.

    The Whole Code

    I explained this rather haphazardly. If you understand Swift, you’ll probably want to look at the full code. You can find everything in this GitHub repository.

  • LucaERP 10: ForEach and Binding Variables

    LucaERP 10: ForEach and Binding Variables

    I’m building an ERP system from scratch to explain all the issues involved in making an ERP. Unfortunately, I’m late again on an installment of Luca ERP due to a bug in the code I was supposed to write. The bug is due to a Swift UI feature: my improper use of ForEach. As an aside to my usual work, I wanted to write about it in case anyone else gets confused by it. For those working in SwiftUI, I want to explain the problem of ForEach with binding here and build a foundation we’ll build our template on next time.

    Let’s start with this table of data, which is a list of people involved with the Huli Pizza Company restaurants.

    I can convert this to a table in Swift.

    enum Role:String{
        case founder = "Founder"
        case vendor = "Vendor"
        case employee = "Employee"
        case customer = "Customer"
        case ohana = "Ohana"
        case regulator = "regulator"
    }
    
    struct aRow: Identifiable{
        var name: String
        var id: Int
        var role: Role = .customer
    }
    
    @Observable class AModel{
        var table:[aRow] = []
        init(){
            table = [
                aRow(name:"Nova",id:0,role:.founder),
                aRow(name:"David",id:1,role:.founder),
                aRow(name:"Keiko",id:2,role:.founder),
                aRow(name:"George",id:3,role:.founder),
                aRow(name:"Craig",id:4,role:.vendor),
                aRow(name:"Carmen",id:5,role:.ohana),
                aRow(name:"Steve",id:6, role:.regulator),
                aRow(name:"Auntie Maise",id:6, role:.ohana),
                aRow(name:"Kai",id:7),
                aRow(name:"Jesse",id:8,role:.employee),
                aRow(name:"Sara",id:9,role:.employee),
                aRow(name:"Tia",id:10,role:.employee),
                aRow(name:"Ralph",id:11,role:.employee),
                aRow(name:"Jorge",id:12,role:.employee),
                aRow(name:"Mark",id:13,role:.employee)
                
            ]
        }
    }
    

    In my view, I can declare this table in two ways:

    @State var aModel = AModel()
    @State var table = AModel().table
    

    The first gives me access to any other properties and methods available on AModel, while the second is the table alone.

    For the simplest use of ForEach, with a little formatting, we get:

    Text("Basic ForEach, all values").font(.title)
    ScrollView{
        ForEach(table){row in
            HStack{
                Text(row.name).frame(width:150)
                Text(row.role.rawValue).frame(width:150)
                Spacer()
            }
        }
    }
    .padding(.bottom,10)
    

    This code gives us a list of people and their roles in the company.

    If we wanted to edit the name, we’d need a text field.

    I’ll make another copy of this list under the first using a TextField instead of Text.

    Text("Binding ForEach").font(.title)
    ScrollView{
        ForEach(table){row in
            TextField("Name", text: row.name).frame(width:150)
            Text(row.role.rawValue).frame(width:150)
        }
    }
    .padding(.bottom,10)
    

    However, that gets me an error:

    Cannot convert value of type 'String' to expected argument type 'Binding'

    The error is due to the text: parameter of TextField being binding. A @Binding variable can send data down the hierarchy into subviews and, if changed there, reflect that value up the view hierarchy. In most cases, you signify something is binding with a $ prefix to a @State or another @Binding variable.

    TextField("Name",text:$row.name).frame(width:150)
    

    Making this change gives me a new error message

    Cannot find '$row' in scope

    The row in my ForEach is neither a binding nor a state variable, so the binding version doesn’t exist. I have to declare this as binding. SwiftUI lets me declare this in one place: the object I’m iterating over that can be binding, in this case, table.

    Depending on the Xcode version, you get different error messages. In playgrounds on iOS 18, I got

    Generic parameter 'V' could not be inferred

    and in Xcode26 Beta 3

    Cannot assign to property: 'rawValue' is immutable

    Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'

    Both errors mean the same thing, though Xcode 26 explains it better on the first line. We’ve satisfied the binding for the text: parameter, but now that row is binding, something that isn’t binding like the role’s rawValue, which is a constant, doesn’t like it.

    There are two ways of handling this. One is to use a wrapped value on these non-binding values.

    Text(row.wrappedValue.role.rawValue).frame(width:150)
    

    The other is to indicate that row is binding with $row

    ForEach($table){$row in
        HStack{
            TextField("Name",text:$row.name).frame(width:150)
            Text(row.role.rawValue).frame(width:150)
        }
    }
    

    This second code snippet is more consistent with SwiftUI code and, thus, preferred.

    While I can’t replicate it here, it also confuses the compiler, giving the unable to evaluate type message.

    With the code we’ve written, we can edit the list. I can change David to Chef David and Carmen to her nom de gurre for as a roller derby blocker for example.

    I’m going to add two more features as part of the foundation of the hybrid template.

    I’ll add another variable to indicate selected rows.

    @State var selected: Int! = nil
    

    I’ll make every row a button, which, when tapped, selects the row, highlighting it.

    Button{
        selected = row.id
    } label:{
        HStack{
            TextField("Name", text: row.name).frame(width:150)
            Text(row.wrappedValue.role.rawValue).frame(width:150)
            Spacer()
        }
        .background(.yellow.opacity(selected == row.id ? 1.0 : 0.01))
    }
    

    This highlighting works well, making it easier to see which row we’re working with.

    We have two instances of the table, aModel.table and table. If I use aModel.table in the lower half and table in the upper half, I will see no updating in the upper half, since it is a different table. Make sure those match up.

    Let’s say I want to show only founders. I could use a filter to do that. The filter method takes a predicate to show only true cases for the predicate. I’ll discuss predicates in more detail in the next newsletter, but they are closures that return a Boolean value. I’ll change the top ForEach to use aModel and filter for founders.

    ForEach(aModel.table.filter{$0.role == .founder}){row in
    

    That code gets me founders as a read-only list. I’ll do the same on the bottom one,

    ForEach($aModel.table.filter(predicate)){row in
    

    All hell breaks loose on error messages.

    Cannot call value of non-function type 'Binding Bool) throws -< [aRow]>'

    Cannot infer contextual base in reference to member 'founder'

    Dynamic key path member lookup cannot refer to instance method 'filter'

    The problem here is the same as before. I used $0 to designate the row in the filter, but row is not a binding variable. I have to write this differently to make sure I’m comparing to a binding variable.

    ForEach($aModel.table.filter{$row in row.role == .founder}){$row in
    

    Which filters the binding rows. The binding rows with a filter have one other purpose – as a child table. For instance, consider an app that searches for people based on their role at Huli Pizza Company. Our role table looks like this.

    It will use our aModel data as a child, showing only the people with that role.

    I’ll add this model to the app:

    struct RoleDescriptor: Identifiable{
        var id: Role
        var description: String
    }
    
    class Roles{
        var roles:[RoleDescriptor] = [
            RoleDescriptor(id: .founder, description: "The original investors in HPC"),
            RoleDescriptor(id: .vendor, description: "People who we pay for goods and services(A/P)"),
            RoleDescriptor(id: .employee, description: "People we pay to work in our facilities"),
            RoleDescriptor(id: .ohana, description: "Friends and family"),
            RoleDescriptor(id: .regulator, description: "Local, State and Federal Government people. "),
            RoleDescriptor(id: .customer, description: "People who love our pizza so much, they give us money for it"),
            
        ]
    }        
    

    We have role descriptors, based on the Enum Role. I’ll select a role by moving through these individually, showing the people in each of these role categories. The role becomes the parent, and the model becomes a child view.

    I’ll add another state variable for the roles and an array index. Usually I’d use a picker for this, but for illustration purposes I want to show one at a time, and so I’ll work with the array indices since the id is non-numeric.

    @State var roles = Roles().roles
    @State var index:Int = 0
    

    At the top of the VStack, I’ll add this to display my new parent data:

    Text(roles[index].id.rawValue)
       .font(.largeTitle).bold()
    Text(roles[index].description)
    Divider()
    

    The child views use role[index].id in their predicates, filtering to the current role only.

    ForEach(aModel.table.filter{$0.role == roles[index].id}){row in
    ...
    ForEach($aModel.table.filter{$row in row.role == roles[index].id}){$row in
    

    And finally, a next button at the bottom.

    Button("Next"){
       index = (index + 1) % roles.count
    }
    .font(.title)
    .buttonStyle(.borderedProminent)
    

    We have the basics of a parent with the child tables changing with it.

    This code is the basis for displaying many types of forms, like invoices, bills of materials, and sales orders. A template for this will be the core of LucaERP’s hybrid template.

    While preparing this newsletter, I encountered an issue that prevented me from providing the hybrid template as intended. However, I got to explore with you SwiftUI and how ForEach works with binding variables, and why you need to be careful to track all of them in a ForEach loop.

    In the next LucaP newsletter, we’ll look at the rest of the template and put it together, based on this foundation. We have a lot to discuss when we start talking about dependencies of parent-child tables adopting Crudable, flexibility in implementation, and getting the best user experience in a tablet environment.

    The Whole Code

    This week’s code is available as a single file here on GitHub or you can cut and paste the code below into Xcode or Swift Playgrounds.

    //
    //  ContentView.swift
    //  ForEach Demo
    //
    //  Created by Steven Lipton on 7/11/25.
    //
    
    import SwiftUI
    
    enum Role:String{
        case founder = "Founder"
        case vendor = "Vendor"
        case employee = "Employee"
        case customer = "Customer"
        case ohana = "Ohana"
        case regulator = "Regulator"
    }
    
    struct aRow:Identifiable{
        var name:String
        var id:Int
        var role:Role = .customer
    }
    
    @Observable class AModel{
        var table:[aRow] = []
        init(){
            table = [
                aRow(name:"Nova",id:0,role:.founder),
                aRow(name:"David",id:1,role:.founder),
                aRow(name:"Keiko",id:2,role:.founder),
                aRow(name:"George",id:3,role:.founder),
                aRow(name:"Craig",id:4,role:.vendor),
                aRow(name:"Carmen",id:5,role:.ohana),
                aRow(name:"Steve",id:6, role:.regulator),
                aRow(name:"Auntie Maise",id:7, role:.ohana),
                aRow(name:"Kai",id:8,role:.customer),
                aRow(name:"Jesse",id:9,role:.employee),
                aRow(name:"Sara",id:10,role:.employee),
                aRow(name:"Tia",id:11,role:.employee),
                aRow(name:"Ralph",id:12,role:.employee),
                aRow(name:"Jorge",id:13,role:.employee),
                aRow(name:"Mark",id:14,role:.employee)
                
            ]
        }
    }
    
    
    struct RoleDescriptor:Identifiable{
        var id:Role
        var description:String
    }
    
    class Roles{
        var roles:[RoleDescriptor] = [
            RoleDescriptor(id: .founder, description: "The original investors in HPC"),
            RoleDescriptor(id: .vendor, description: "People who we pay for goods and services(A/P)"),
            RoleDescriptor(id: .employee, description: "People we pay to work in our facilities"),
            RoleDescriptor(id: .ohana, description: "Friends and family"),
            RoleDescriptor(id: .regulator, description: "Local, State and Federal Government people. "),
            RoleDescriptor(id: .customer, description: "People who love our pizza so much, they give us money for it"),
            
        ]
    }
    
    struct ContentView: View {
        @State var aModel = AModel()
        @State var table = AModel().table
        @State var selected:Int! = nil
        @State var roles = Roles().roles
        @State var index:Int = 0
        
        
        var body: some View {
            VStack(alignment:.leading) {
                VStack{
                    Text(roles[index].id.rawValue)
                        .font(.largeTitle).bold()
                    Text(roles[index].description)
                    Divider()
                }
                .glassEffect()
               
                
                Text("Basic ForEach, all values").font(.title)
                ScrollView{
                    ForEach(aModel.table.filter{$0.role == .founder}){row in
                        HStack{
                            Text(row.name)
                                .frame(width:150)
                            Text(row.role.rawValue).frame(width:150)
                            Spacer()
                        }
                    }
                }
                .padding()
                
                .padding(.bottom,10)
                
                Text("Binding ForEach").font(.title)
                ScrollView{
                    ForEach($aModel.table.filter{$row in row.role == roles[index].id}){$row in
                        Button{
                            selected = row.id
                        } label:{
                            HStack{
                                TextField("Name",text:$row.name).frame(width:150)
                                Text(row.role.rawValue).frame(width:150)
                                Spacer()
                            }
                            .background(.yellow.opacity(selected == row.id ? 1.0 : 0.01))
                        }
                    }
                }
                .padding()
                
                
                .padding(.bottom,10)
                Button("Next"){
                    index = (index + 1) % roles.count
                }
                .font(.title)
                .buttonStyle(.borderedProminent)
            }
            .padding()
            
            
            
            
            
        }
    }
    
    #Preview {
        ContentView()
    }