Skip to content

Commit

Permalink
convert GnuCash skr49 german account hierarchy to OpenPetra yml file.
Browse files Browse the repository at this point in the history
fixes for importing account hierarchy.
print proper error messages if the yml format is broken.
fixes #293
  • Loading branch information
tpokorra committed Jun 2, 2021
1 parent 938a5d0 commit a72ce49
Show file tree
Hide file tree
Showing 8 changed files with 2,020 additions and 15 deletions.
2 changes: 2 additions & 0 deletions csharp/ICT/Common/IO/Yml2Xml.cs
Expand Up @@ -756,6 +756,8 @@ private void ParseNode(XmlNode parent, int ADepth)
}

PrintedOriginalError = true;

throw new Exception("Problem in line " + currentLine.ToString() + " " + line);
}

throw;
Expand Down
50 changes: 36 additions & 14 deletions csharp/ICT/Petra/Server/lib/MFinance/setup/GL.Setup.cs
Expand Up @@ -1046,6 +1046,7 @@ public static GLSetupTDS LoadAccountHierarchies(Int32 ALedgerNumber)
AFeesPayableAccess.LoadViaALedger(MainDS, ALedgerNumber, Transaction);
AGeneralLedgerMasterAccess.LoadUsingTemplate(MainDS, template, Transaction);
ASuspenseAccountAccess.LoadViaALedger(MainDS, ALedgerNumber, Transaction);
ATransactionTypeAccess.LoadViaALedger(MainDS, ALedgerNumber, Transaction);
});

// set Account BankAccountFlag if there exists a property
Expand Down Expand Up @@ -1191,11 +1192,15 @@ public static string LoadAccountHierarchyHtmlCode(Int32 ALedgerNumber, string AA
AAccountHierarchyDetailTable.GetAccountHierarchyCodeDBName() + " = '" + AAccountHierarchyCode + "' AND " +
AAccountHierarchyDetailTable.GetAccountCodeToReportToDBName() + " = '" + accountHierarchy.RootAccountCode + "'";

result.Append(InsertNodeIntoHTMLTreeView(

foreach (DataRowView vrow in MainDS.AAccountHierarchyDetail.DefaultView)
{
result.Append(InsertNodeIntoHTMLTreeView(
MainDS,
ALedgerNumber,
(AAccountHierarchyDetailRow)MainDS.AAccountHierarchyDetail.DefaultView[0].Row,
(AAccountHierarchyDetailRow)vrow.Row,
true));
}
}

return result.ToString();
Expand Down Expand Up @@ -2929,8 +2934,12 @@ public static bool ExportAccountHierarchy(Int32 ALedgerNumber, string AAccountHi
AAccountHierarchyDetailTable.GetAccountHierarchyCodeDBName() + " = '" + AAccountHierarchyName + "' AND " +
AAccountHierarchyDetailTable.GetAccountCodeToReportToDBName() + " = '" + accountHierarchy.RootAccountCode + "'";

InsertNodeIntoXmlDocument(MainDS, xmlDoc, xmlDoc.DocumentElement,
(AAccountHierarchyDetailRow)MainDS.AAccountHierarchyDetail.DefaultView[0].Row);

foreach (DataRowView vrow in MainDS.AAccountHierarchyDetail.DefaultView)
{
InsertNodeIntoXmlDocument(MainDS, xmlDoc, xmlDoc.DocumentElement,
(AAccountHierarchyDetailRow)vrow.Row);
}
}
}
catch (Exception ex)
Expand Down Expand Up @@ -3276,15 +3285,17 @@ public static bool ImportAccountHierarchy(Int32 ALedgerNumber, string AHierarchy
TYml2Xml ymlParser = new TYml2Xml(AYmlAccountHierarchy.Split(new char[] { '\n' }));
XMLDoc = ymlParser.ParseYML2XML();
}
catch (XmlException exp)
catch (Exception exp)
{
TLogging.Log(exp.ToString());
throw new Exception(
Catalog.GetString("There was a problem with the syntax of the file.") +
Environment.NewLine +
exp.Message +
Environment.NewLine +
AYmlAccountHierarchy);
AVerificationResult.Add(new TVerificationResult(
Catalog.GetString("Import hierarchy"),
"base64" + THttpBinarySerializer.SerializeToBase64(
Catalog.GetString("There was a problem with the syntax of the file: ") +
Environment.NewLine +
exp.Message),
TResultSeverity.Resv_Critical));
return false;
}
}

Expand All @@ -3304,7 +3315,11 @@ public static bool ImportAccountHierarchy(Int32 ALedgerNumber, string AHierarchy
}
}

CreateAccountHierarchyRecursively(ref MainDS, ALedgerNumber, ref ImportedAccountNames, Root, ALedgerNumber.ToString(), ref AVerificationResult);
while (Root != null)
{
CreateAccountHierarchyRecursively(ref MainDS, ALedgerNumber, ref ImportedAccountNames, Root, ALedgerNumber.ToString(), ref AVerificationResult);
Root = Root.NextSibling;
}

foreach (AAccountRow accountRow in MainDS.AAccount.Rows)
{
Expand All @@ -3325,9 +3340,16 @@ public static bool ImportAccountHierarchy(Int32 ALedgerNumber, string AHierarchy

if (transTbl.Rows.Count == 0) // No-one's used this account, so I can delete it.
{
//
// If the deleted account included Analysis types I need to unlink them from the Account first.
// remove transaction types if they reference the account
foreach (ATransactionTypeRow Row in MainDS.ATransactionType.Rows)
{
if ((Row.RowState != DataRowState.Deleted) && (Row.LedgerNumber == ALedgerNumber) && (Row.DebitAccountCode == accountRow.AccountCode || Row.CreditAccountCode == accountRow.AccountCode))
{
Row.Delete();
}
}

// If the deleted account included Analysis types I need to unlink them from the Account first.
foreach (AAnalysisAttributeRow Row in MainDS.AAnalysisAttribute.Rows)
{
if ((Row.LedgerNumber == ALedgerNumber) && (Row.AccountCode == accountRow.AccountCode))
Expand Down
1 change: 1 addition & 0 deletions demodata/finance/skr49/.gitignore
@@ -0,0 +1 @@
acctchrt_skr49.gnucash-xea
9 changes: 9 additions & 0 deletions demodata/finance/skr49/README.md
@@ -0,0 +1,9 @@
We are using the SKR49 file from GnuCash, convert it from XML to our yml format with a Python script, and then you can import it into OpenPetra.

```
curl https://raw.githubusercontent.com/Gnucash/gnucash/maint/data/accounts/de_DE/acctchrt_skr49.gnucash-xea > acctchrt_skr49.gnucash-xea
export LANG=de_DE.utf8
python3 convert.py > skr49_accounts.yml
```

Now import that file `skr49_accounts.yml` to your OpenPetra instance, https://openpetra.example.org/Finance/Setup/GL/AccountTree
144 changes: 144 additions & 0 deletions demodata/finance/skr49/convert.py
@@ -0,0 +1,144 @@
#!/bin/python3

import xml.etree.ElementTree as ET
import os

gnucash_skr49_url = "https://raw.githubusercontent.com/Gnucash/gnucash/maint/data/accounts/de_DE/acctchrt_skr49.gnucash-xea"
if not os.path.isfile(os.path.basename(gnucash_skr49_url)):
print("Please download: curl %s > %s" % (gnucash_skr49_url,os.path.basename(gnucash_skr49_url)))
exit(-1)

if not ("LANG" in os.environ and os.environ["LANG"] == "de_DE.utf8"):
print("Please export LANG=de_DE.utf8")
exit(-1)

mytree = ET.parse(os.path.basename(gnucash_skr49_url))
myroot = mytree.getroot()
namespaces = {'gnc': 'http://www.gnucash.org/XML/gnc','act':'http://www.gnucash.org/XML/act', 'gnc-act':'http://www.gnucash.org/XML/gnc-act'}

class Account(object):
pass

def export_account(parent, accounts, depth):
children = {}
for acct_id in accounts:
account = accounts[acct_id]
if account.parent == parent.id:
children[account.code] = account

for code in sorted(children):
account = children[code]

is_leaf = True
for acct_child_id in accounts:
acct_child = accounts[acct_child_id]
if acct_child.parent == account.id:
is_leaf = False
break

active="True"
if account.descr == "VERALTET!":
active = "False"
line = ((depth*2)*' ')
line += '%s: {active=%s' % (account.code, active)

if parent.parent is None:
line += ', type=%s, debitcredit=%s, validcc=All' % (account.type, account.debitcredit)
else:
if parent.type != account.type:
line += ', type=%s' % (account.type,)
if parent.debitcredit != account.debitcredit:
line += ', debitcredit=%s' % (account.debitcredit,)
if is_leaf:
line += ', validcc=Local'
if account.descr == account.shortdescr:
line += ', shortdesc="%s"' % (account.descr)
else:
line += ', shortdesc="%s", longdesc="%s"' % (account.shortdescr, account.descr)
if account.bankaccount == True:
line += ', bankaccount=true'
line += "}"
print(line)

if not is_leaf:
export_account(account, accounts, depth + 1)


accounts = {}
root_account = None

# parse all accounts
for acct in myroot.findall('.//gnc:account', namespaces):
obj = Account()
obj.id = acct.find('act:id', namespaces).text
if acct.find('act:parent', namespaces) is not None:
obj.parent = acct.find('act:parent', namespaces).text
else:
obj.parent = None
obj.name = acct.find('act:name', namespaces).text
if acct.find('act:code', namespaces) is not None:
obj.code = acct.find('act:code', namespaces).text
else:
# bug: there is a missing code, in 4500-4504 Abschreibungen
obj.code = obj.name.split(' ')[0]
# bug in account 0674
if obj.name == "0675 Gegenkonto 0653-0654, 0661-0664, 0670-0672, 0675-0679, 0687-0689, 0697-0699 bei Aufteilung Debitorenkonto":
obj.code = "0674"
obj.name = "0674 Gegenkonto 0653-0654, 0661-0664, 0670-0672, 0675-0679, 0687-0689, 0697-0699 bei Aufteilung Debitorenkonto"
# bug: duplicate code 5490
if obj.code == "5490" and obj.id == "0467d26561464460851449d59cb02696":
# ignore this
continue
obj.shortdescr = obj.name[len(obj.code) + 1:]
if acct.find('act:description', namespaces) is not None:
obj.descr = acct.find('act:description', namespaces).text.replace("\n", " ")
else:
obj.descr = ''
obj.type = acct.find('act:type', namespaces).text
obj.bankaccount = False
if obj.type == "ASSET" or obj.type == "ROOT":
obj.type = "Asset"
obj.debitcredit = "debit"
elif obj.type == "INCOME":
obj.type = "Income"
obj.debitcredit = "credit"
elif obj.type == "EXPENSE":
obj.type = "Expense"
obj.debitcredit = "debit"
elif obj.type == "LIABILITY":
obj.type = "Liability"
obj.debitcredit = "credit"
elif obj.type == "CASH" or obj.type == "BANK":
obj.type = "Asset"
obj.debitcredit = "debit"
obj.bankaccount = True
elif obj.type == "PAYABLE":
obj.type = "Liability"
obj.debitcredit = "credit"
elif obj.type == "RECEIVABLE":
obj.type = "Asset"
obj.debitcredit = "debit"
elif obj.type == "CREDIT":
obj.type = "Liability"
obj.debitcredit = "credit"
else:
raise Exception("unknown type %s for account %s" % (obj.type,obj.name))

if obj.parent is None:
root_account = obj
else:
accounts[obj.id] = obj

# export hierarchy
title = myroot.findall('.//gnc-act:title', namespaces)[0].text
shortdesc = myroot.findall('.//gnc-act:short-description', namespaces)[0].text
longdesc = myroot.findall('.//gnc-act:long-description', namespaces)[0].text.replace('\n', '\n# ')

print("# %s" % (title,))
print("#")
print("# %s" % (shortdesc,))
print("#")
print("# %s" % (longdesc,))
print("RootNodeInternal:")
export_account(root_account, accounts, 1)

0 comments on commit a72ce49

Please sign in to comment.