{*******************************************************} { } { Responsive Software http://www.responsive.co.nz } { } { Copyright (c) 2003-2006 Responsive Software Limited } { } {*******************************************************} unit BusinessObjects; interface uses Classes, Grids, GeneralUtilities, DatabaseObjects; type // these classes represent business entities which are NOT stored in the database // they have a common ancestor class which provides virtual // maintenance functions to allow for generic handling TBusinessObject = class procedure Assign (BusinessObject : TBusinessObject); procedure SaveToStream (Stream : TStream); virtual; abstract; procedure LoadFromStream (Stream : TStream); virtual; abstract; constructor Create; virtual; end; TBusinessObjectClass = class of TBusinessObject; TBusinessObjectCollection = class private FObjects : TList; FOwned : boolean; function GetCount : integer; function GetObject (i : integer) : TBusinessObject; public constructor Create; virtual; destructor Destroy; override; procedure Add (BusinessObject : TBusinessObject); procedure Insert (Index : integer; BusinessObject : TBusinessObject); function IndexOf (BusinessObject : TBusinessObject) : integer; procedure Delete (Index : integer); procedure Clear; procedure Sort (Compare : TListSortCompare); procedure SaveToStream (Stream : TStream); procedure LoadFromStream (Stream : TStream); property Count : integer read GetCount; property Objects[i : integer] : TBusinessObject read GetObject; default; property Owned : boolean read FOwned write FOwned; end; // classes used to describe the layout of balance sheets // and income statements TReportHeadingElement = class(TBusinessObject) Text : string; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; end; TReportGroupElement = class(TBusinessObject) Text : string; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; end; TReportAccountElement = class(TBusinessObject) AccountId : int64; InGroup : boolean; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; function AccountName : string; function AccountType : TAccountType; end; TReportSubtotalElement = class(TBusinessObject) Text : string; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; end; TReportTotalElement = class(TBusinessObject) Text : string; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; end; TReportRetainedEarningsElement = class(TBusinessObject) Text : string; InGroup : boolean; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; end; TReportLayoutType = (rtBalanceSheet,rtIncomeStatement); TReportLayout = class(TBusinessObject) CompanyId : int64; Elements : TBusinessObjectCollection; ReportLayoutType : TReportLayoutType; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; constructor Create; override; destructor Destroy; override; function IncludesAccount (AccountId : int64) : boolean; procedure CreateDefaultBalanceSheet; procedure UpdateBalanceSheet; procedure CreateDefaultIncomeStatement; procedure UpdateIncomeStatement; end; // this class is used to hold POS configuration information // which is stored in the registry // new fields should be added at the end TPOSConfiguration = class(TBusinessObject) POSDataDirectory : string; ControlColor : integer; MaximiseOnStart : boolean; PrintReceipt : boolean; procedure SaveToStream (Stream : TStream); override; procedure LoadFromStream (Stream : TStream); override; procedure SetDefaults; end; procedure RegisterBusinessClasses; function ConvertBusinessObjectClassToInteger (BusinessObjectClass : TBusinessObjectClass) : integer; function ConvertIntegerToBusinessObjectClass (Number : integer) : TBusinessObjectClass; implementation uses NMFTP, Dialogs, Globals, Utilities, SysUtils, DatabaseManager; var BusinessClasses : array[1..7] of TBusinessObjectClass; procedure RegisterBusinessClasses; var i : integer; begin i := 0; Inc(i); BusinessClasses[i] := TReportHeadingElement; Inc(i); BusinessClasses[i] := TReportAccountElement; Inc(i); BusinessClasses[i] := TReportGroupElement; Inc(i); BusinessClasses[i] := TReportSubtotalElement; Inc(i); BusinessClasses[i] := TReportTotalElement; Inc(i); BusinessClasses[i] := TReportRetainedEarningsElement; Inc(i); BusinessClasses[i] := TReportLayout; end; {***** TBusinessObject methods ************************************************} procedure TBusinessObject.Assign (BusinessObject : TBusinessObject); var StringStream : TStringStream; begin StringStream := TStringStream.Create(''); BusinessObject.SaveToStream(StringStream); StringStream.Position := 0; Self.LoadFromStream(StringStream); StringStream.Free; end; constructor TBusinessObject.Create; begin inherited; end; {***** TBusinessObjectCollection methods **************************************} // create a new empty business object collection constructor TBusinessObjectCollection.Create; begin inherited Create; FObjects := TList.Create; end; destructor TBusinessObjectCollection.Destroy; var i : integer; begin if FOwned then begin for i := 0 to FObjects.Count - 1 do TBusinessObject(FObjects.Items[i]).Free; end; FObjects.Free; inherited; end; // add a business object to the collection procedure TBusinessObjectCollection.Add (BusinessObject : TBusinessObject); begin FObjects.Add(BusinessObject); end; // insert a business object into the collection in the given position procedure TBusinessObjectCollection.Insert (Index : integer; BusinessObject : TBusinessObject); begin FObjects.Insert(Index,BusinessObject); end; // obtain the position of a business object in the collection function TBusinessObjectCollection.IndexOf (BusinessObject : TBusinessObject) : integer; begin Result := FObjects.IndexOf(BusinessObject); end; // delete a business object from the collection in the given position procedure TBusinessObjectCollection.Delete (Index : integer); begin if FOwned then TBusinessObject(FObjects.Items[Index]).Free; FObjects.Delete(Index); end; // remove all objects from collection procedure TBusinessObjectCollection.Clear; var i : integer; begin if FOwned then begin for i := 0 to FObjects.Count - 1 do TBusinessObject(FObjects.Items[i]).Free; end; FObjects.Clear; end; procedure TBusinessObjectCollection.Sort (Compare : TListSortCompare); begin FObjects.Sort(Compare); end; procedure TBusinessObjectCollection.SaveToStream (Stream : TStream); var i : integer; Count : integer; ClassId : integer; begin Count := FObjects.Count; Stream.Write(Count,SizeOf(Count)); for i := 0 to FObjects.Count - 1 do begin ClassId := ConvertBusinessObjectClassToInteger (TBusinessObjectClass(TObject(FObjects.Items[i]).ClassType)); Stream.Write(ClassId,SizeOf(ClassId)); TBusinessObject(FObjects.Items[i]).SaveToStream(Stream); end; end; procedure TBusinessObjectCollection.LoadFromStream (Stream : TStream); var i : integer; Count : integer; ClassId : integer; BusinessObject : TBusinessObject; BusinessObjectClass : TBusinessObjectClass; begin Stream.Read(Count,SizeOf(Count)); for i := 0 to Count - 1 do begin Stream.Read(ClassId,SizeOf(ClassId)); BusinessObjectClass := ConvertIntegerToBusinessObjectClass(ClassId); BusinessObject := BusinessObjectClass.Create; Add(BusinessObject); BusinessObject.LoadFromStream(Stream); end; end; // return number of objects in collection function TBusinessObjectCollection.GetCount : integer; begin Result := FObjects.Count; end; function TBusinessObjectCollection.GetObject (i : integer) : TBusinessObject; begin if (i >= 0) and (i < FObjects.Count) then Result := TBusinessObject(FObjects.Items[i]) else Result := nil; end; {***** Miscellaneous methods **************************************************} {***** TReportHeadingElement methods ******************************************} procedure TReportHeadingElement.SaveToStream (Stream : TStream); begin WriteStrToStream(Text,Stream); end; procedure TReportHeadingElement.LoadFromStream (Stream : TStream); begin Text := ReadStrFromStream(Stream); end; {***** TReportGroupElement methods ********************************************} procedure TReportGroupElement.SaveToStream (Stream : TStream); begin WriteStrToStream(Text,Stream); end; procedure TReportGroupElement.LoadFromStream (Stream : TStream); begin Text := ReadStrFromStream(Stream); end; {***** TReportAccountElement **************************************************} procedure TReportAccountElement.SaveToStream (Stream : TStream); begin Stream.Write(AccountId,SizeOf(AccountId)); Stream.Write(InGroup,SizeOf(InGroup)); end; procedure TReportAccountElement.LoadFromStream (Stream : TStream); begin Stream.Read(AccountId,SizeOf(AccountId)); Stream.Read(InGroup,SizeOf(InGroup)); end; function TReportAccountElement.AccountName : string; begin Result := Utilities.AccountName(AccountId); if Result = '' then Result := ''; end; function TReportAccountElement.AccountType : TAccountType; begin Result := Utilities.AccountType(AccountId); end; {***** TReportSubtotalElement *************************************************} procedure TReportSubtotalElement.SaveToStream (Stream : TStream); begin WriteStrToStream(Text,Stream); end; procedure TReportSubtotalElement.LoadFromStream (Stream : TStream); begin Text := ReadStrFromStream(Stream); end; {***** TReportTotalElement methods ********************************************} procedure TReportTotalElement.SaveToStream (Stream : TStream); begin WriteStrToStream(Text,Stream); end; procedure TReportTotalElement.LoadFromStream (Stream : TStream); begin Text := ReadStrFromStream(Stream); end; {***** TReportRetainedEarningsElement *****************************************} procedure TReportRetainedEarningsElement.SaveToStream (Stream : TStream); begin WriteStrToStream(Text,Stream); Stream.Write(InGroup,SizeOf(InGroup)); end; procedure TReportRetainedEarningsElement.LoadFromStream (Stream : TStream); begin Text := ReadStrFromStream(Stream); Stream.Read(InGroup,SizeOf(InGroup)); end; {***** TReportLayout methods **************************************************} function CompareReportAccountElements (Item1, Item2 : pointer) : integer; var ReportAccountElement1, ReportAccountElement2 : TReportAccountElement; begin ReportAccountElement1 := TReportAccountElement(Item1); ReportAccountElement2 := TReportAccountElement(Item2); if (ReportAccountElement1 = nil) and (ReportAccountElement2 = nil) then Result := 0 else if (ReportAccountElement1 = nil) and (ReportAccountElement2 <> nil) then Result := -1 else if (ReportAccountElement1 <> nil) and (ReportAccountElement2 = nil) then Result := 1 else begin // compare on account type Result := integer(ReportAccountElement1.AccountType) - integer(ReportAccountElement2.AccountType); // if same then use account name if Result = 0 then Result := CompareText( ReportAccountElement1.AccountName, ReportAccountElement2.AccountName); end; end; procedure TReportLayout.SaveToStream (Stream : TStream); begin Stream.Write(CompanyId,SizeOf(CompanyId)); Stream.Write(ReportLayoutType,SizeOf(ReportLayoutType)); Elements.SaveToStream(Stream); end; procedure TReportLayout.LoadFromStream (Stream : TStream); begin Stream.Read(CompanyId,SizeOf(CompanyId)); Stream.Read(ReportLayoutType,SizeOf(ReportLayoutType)); Elements.Clear; Elements.LoadFromStream(Stream); end; constructor TReportLayout.Create; begin Elements := TBusinessObjectCollection.Create; Elements.Owned := true; end; destructor TReportLayout.Destroy; begin Elements.Free; end; function TReportLayout.IncludesAccount (AccountId : int64) : boolean; var i : integer; begin Result := false; for i := 0 to Elements.Count - 1 do if Elements[i] is TReportAccountElement then if TReportAccountElement(Elements[i]).AccountId = AccountId then begin Result := true; Exit; end; end; procedure TReportLayout.CreateDefaultBalanceSheet; var i : integer; Account : TAccount; ReportAccountElement : TReportAccountElement; ReportHeadingElement : TReportHeadingElement; ReportSubtotalElement : TReportSubtotalElement; ReportTotalElement : TReportTotalElement; ReportRetainedEarningsElement : TReportRetainedEarningsElement; FirstTotalAdded : boolean; AssetAccountFound : boolean; LiabilityAccountFound : boolean; EquityAccountFound : boolean; begin Elements.Clear; for i := 0 to Globals.Accounts.Count - 1 do begin Account := TAccount(Globals.Accounts[i]); if (Account.CompanyId = CompanyId) and (Account.BalanceSheet) then begin // if the account belongs to this company and is a balance sheet type // account then create an account element for it and add to the list ReportAccountElement := TReportAccountElement.Create; ReportAccountElement.AccountId := Account.Id; Elements.Add(ReportAccountElement); end; end; // now sort the elements into order with the assets first // then the liabilities and then the equity Elements.Sort(CompareReportAccountElements); // add two total elements // one after assets and before liabilities and equity FirstTotalAdded := false; i := 0; while i <= Elements.Count - 1 do begin if ( (Elements[i] is TReportAccountElement) and (TReportAccountElement(Elements[i]).AccountType in [atLiability,atEquity])) then begin ReportTotalElement := TReportTotalElement.Create; ReportTotalElement.Text := 'TOTAL'; Elements.Insert(i,ReportTotalElement); FirstTotalAdded := true; Break; end; Inc(i); end; if not FirstTotalAdded then begin ReportTotalElement := TReportTotalElement.Create; ReportTotalElement.Text := 'TOTAL'; Elements.Add(ReportTotalElement); end; // add retained earnings element before last total ReportRetainedEarningsElement := TReportRetainedEarningsElement.Create; ReportRetainedEarningsElement.Text := 'Retained Earnings'; Elements.Add(ReportRetainedEarningsElement); // the other total at the end ReportTotalElement := TReportTotalElement.Create; ReportTotalElement.Text := 'TOTAL'; Elements.Add(ReportTotalElement); // add default headings i := 0; while i <= Elements.Count - 1 do begin if ( (Elements[i] is TReportAccountElement) and (TReportAccountElement(Elements[i]).AccountType = atAsset) ) then begin ReportHeadingElement := TReportHeadingElement.Create; ReportHeadingElement.Text := 'ASSETS'; Elements.Insert(i,ReportHeadingElement); Break; end; Inc(i); end; i := 0; while i <= Elements.Count - 1 do begin if ( (Elements[i] is TReportAccountElement) and (TReportAccountElement(Elements[i]).AccountType = atLiability) ) then begin ReportHeadingElement := TReportHeadingElement.Create; ReportHeadingElement.Text := 'LIABILITIES'; Elements.Insert(i,ReportHeadingElement); Break; end; Inc(i); end; i := 0; while i <= Elements.Count - 1 do begin if ( (Elements[i] is TReportAccountElement) and (TReportAccountElement(Elements[i]).AccountType = atEquity) ) then begin ReportHeadingElement := TReportHeadingElement.Create; ReportHeadingElement.Text := 'EQUITY'; Elements.Insert(i,ReportHeadingElement); Break; end; Inc(i); end; // initialise flags AssetAccountFound := false; LiabilityAccountFound := false; EquityAccountFound := false; // add default subtotals i := 0; while i <= Elements.Count - 1 do begin if (Elements[i] is TReportAccountElement) then begin if TReportAccountElement(Elements[i]).AccountType = atAsset then AssetAccountFound := true else if TReportAccountElement(Elements[i]).AccountType = atLiability then LiabilityAccountFound := true else if TReportAccountElement(Elements[i]).AccountType = atEquity then EquityAccountFound := true; end; if AssetAccountFound and ( (not (Elements[i] is TReportAccountElement)) or (TReportAccountElement(Elements[i]).AccountType <> atAsset) ) then begin ReportSubtotalElement := TReportSubtotalElement.Create; ReportSubtotalElement.Text := 'TOTAL ASSETS'; Elements.Insert(i,ReportSubtotalElement); AssetAccountFound := false; end; if LiabilityAccountFound and ( (not (Elements[i] is TReportAccountElement)) or (TReportAccountElement(Elements[i]).AccountType <> atLiability) ) then begin ReportSubtotalElement := TReportSubtotalElement.Create; ReportSubtotalElement.Text := 'TOTAL LIABILIES'; Elements.Insert(i,ReportSubtotalElement); LiabilityAccountFound := false; end; if EquityAccountFound and (not (Elements[i] is TReportRetainedEarningsElement)) and ( (not (Elements[i] is TReportAccountElement)) or (TReportAccountElement(Elements[i]).AccountType <> atEquity) ) then begin ReportSubtotalElement := TReportSubtotalElement.Create; ReportSubtotalElement.Text := 'TOTAL EQUITY'; Elements.Insert(i,ReportSubtotalElement); EquityAccountFound := false; end; Inc(i); end; end; procedure TReportLayout.UpdateBalanceSheet; var i : integer; Account : TAccount; ReportAccountElement : TReportAccountElement; Element : TBusinessObject; begin // remove accounts which are no longer valid for the // balance sheet for this company ie. the company // or the account type must have changed (because we // don't allow any accounts to be deleted) for i := Elements.Count - 1 downto 0 do begin Element := Elements[i]; if Element is TReportAccountElement then if not ( (Globals.Accounts.ObjectsById[TReportAccountElement(Element).AccountId] <> nil) and (AccountCompanyId(TReportAccountElement(Element).AccountId) = CompanyId) and (AccountType(TReportAccountElement(Element).AccountId) in [atAsset,atLiability,atEquity])) then Elements.Delete(i); end; // add any new ones for i := 0 to Globals.Accounts.Count - 1 do begin Account := TAccount(Globals.Accounts[i]); if (Account.CompanyId = CompanyId) and (Account.BalanceSheet) and (not IncludesAccount(Account.Id)) then begin ReportAccountElement := TReportAccountElement.Create; ReportAccountElement.AccountId := Account.Id; Elements.Add(ReportAccountElement); end; end; end; procedure TReportLayout.CreateDefaultIncomeStatement; var i : integer; Account : TAccount; ReportAccountElement : TReportAccountElement; ReportHeadingElement : TReportHeadingElement; ReportTotalElement : TReportTotalElement; ReportRetainedEarningsElement : TReportRetainedEarningsElement; FirstTotalAdded : boolean; begin Elements.Clear; for i := 0 to Globals.Accounts.Count - 1 do begin Account := TAccount(Globals.Accounts[i]); if (Account.CompanyId = CompanyId) and (Account.IncomeStatement) then begin // if the account belongs to this company and is an income type // account then create an account element for it and add to the list ReportAccountElement := TReportAccountElement.Create; ReportAccountElement.AccountId := Account.Id; Elements.Add(ReportAccountElement); end; end; // now sort the elements into order with the income first // then the expenses Elements.Sort(CompareReportAccountElements); // add two total elements // one after income and the other after expenses FirstTotalAdded := false; i := 0; while i <= Elements.Count - 1 do begin if ( (Elements[i] is TReportAccountElement) and (TReportAccountElement(Elements[i]).AccountType in [atExpense])) then begin ReportTotalElement := TReportTotalElement.Create; ReportTotalElement.Text := 'TOTAL'; Elements.Insert(i,ReportTotalElement); FirstTotalAdded := true; Break; end; Inc(i); end; if not FirstTotalAdded then begin ReportTotalElement := TReportTotalElement.Create; ReportTotalElement.Text := 'TOTAL'; Elements.Add(ReportTotalElement); end; // the other total at the end ReportTotalElement := TReportTotalElement.Create; ReportTotalElement.Text := 'TOTAL'; Elements.Add(ReportTotalElement); // add retained earnings element after last total ReportRetainedEarningsElement := TReportRetainedEarningsElement.Create; ReportRetainedEarningsElement.Text := 'Net Income'; Elements.Add(ReportRetainedEarningsElement); // add default headings i := 0; while i <= Elements.Count - 1 do begin if ( (Elements[i] is TReportAccountElement) and (TReportAccountElement(Elements[i]).AccountType = atIncome) ) then begin ReportHeadingElement := TReportHeadingElement.Create; ReportHeadingElement.Text := 'INCOME'; Elements.Insert(i,ReportHeadingElement); Break; end; Inc(i); end; i := 0; while i <= Elements.Count - 1 do begin if ( (Elements[i] is TReportAccountElement) and (TReportAccountElement(Elements[i]).AccountType = atExpense) ) then begin ReportHeadingElement := TReportHeadingElement.Create; ReportHeadingElement.Text := 'EXPENSES'; Elements.Insert(i,ReportHeadingElement); Break; end; Inc(i); end; end; procedure TReportLayout.UpdateIncomeStatement; var i : integer; Account : TAccount; ReportAccountElement : TReportAccountElement; Element : TBusinessObject; begin // remove accounts which are no longer valid for the // income statement for this company ie. the company // or the account type must have changed (because we // don't allow any accounts to be deleted) for i := Elements.Count - 1 downto 0 do begin Element := Elements[i]; if Element is TReportAccountElement then if not ( (Globals.Accounts.ObjectsById[TReportAccountElement(Element).AccountId] <> nil) and (AccountCompanyId(TReportAccountElement(Element).AccountId) = CompanyId) and (AccountType(TReportAccountElement(Element).AccountId) in [atIncome,atExpense])) then Elements.Delete(i); end; // add any new ones for i := 0 to Globals.Accounts.Count - 1 do begin Account := TAccount(Globals.Accounts[i]); if (Account.CompanyId = CompanyId) and (Account.IncomeStatement) and (not IncludesAccount(Account.Id)) then begin ReportAccountElement := TReportAccountElement.Create; ReportAccountElement.AccountId := Account.Id; Elements.Add(ReportAccountElement); end; end; end; {***** TPOSConfiguration methods **********************************************} procedure TPOSConfiguration.SaveToStream (Stream : TStream); begin WriteStrToStream(POSDataDirectory,Stream); Stream.Write(ControlColor,SizeOf(ControlColor)); Stream.Write(MaximiseOnStart,SizeOf(MaximiseOnStart)); Stream.Write(PrintReceipt,SizeOf(PrintReceipt)); end; procedure TPOSConfiguration.LoadFromStream (Stream : TStream); begin POSDataDirectory := ReadStrFromStream(Stream); Stream.Read(ControlColor,SizeOf(ControlColor)); Stream.Read(MaximiseOnStart,SizeOf(MaximiseOnStart)); Stream.Read(PrintReceipt,SizeOf(PrintReceipt)); end; procedure TPOSConfiguration.SetDefaults; begin POSDataDirectory := ExeDirectory; ControlColor := DefaultControlColor; MaximiseOnStart := false; PrintReceipt := false; end; {******************************************************************************} function ConvertBusinessObjectClassToInteger (BusinessObjectClass : TBusinessObjectClass) : integer; var i : integer; begin Result := 0; for i := Low(BusinessClasses) to High(BusinessClasses) do if BusinessClasses[i] = BusinessObjectClass then begin Result := i; break; end; end; function ConvertIntegerToBusinessObjectClass (Number : integer) : TBusinessObjectClass; begin Result := TBusinessObject; if (Number >= Low(BusinessClasses)) and (Number <= High(BusinessClasses)) then Result := BusinessClasses[Number]; end; end.