Search Posts

Visits: 346

JP PINT 0.9.3と0.9.2の比較です。

あまり意味のない記述スタイルの変更がありますが、20世紀エンジニアの観点からは、「気付いたから変更する。」のではなく、先ずコーディングスタイルやパターンごとの記述方式を決めた上で、設計方針に従ってコーディングをしないと品質確保が難しいと思いますが、いかがでしょうか。
40年前に海外から導入したソフトウエアの日本版対応作業では、当初気づかなかった潜在不良が後から発見されて、対応作業に困る事が多かった事が思い出されます。

Alignedルール

aligned-ibrp-050-jp(fatal)

[aligned-ibrp-050-jp]-Each Invoice line (ibg-25) MUST be categorized with an Invoiced item tax category code (ibt-151) and Invoiced item tax rate (ibt-152).

contextの変更なし /ubl:InvoiceLine | /cn:CreditNoteLine
testスクリプト変更
0.9.3では、明細行のcac:Itemのみを対象とするように訂正された。0.9.2では、定義されたすべての階層のcac:Itemを対象としていた。

(
  cac:Item/cac:ClassifiedTaxCategory[cac:TaxScheme/(normalize-space(upper-case(cbc:ID))='VAT')]/cbc:ID
) and 
(
  cac:Item/cac:ClassifiedTaxCategory[cac:TaxScheme/(normalize-space(upper-case(cbc:ID))='VAT')]/cbc:Percent
)

0.9.2

(
  //cac:ClassifiedTaxCategory[cac:TaxScheme/(normalize-space(upper-case(cbc:ID))='VAT')]/cbc:ID
) and 
(
  //cac:ClassifiedTaxCategory[cac:TaxScheme/(normalize-space(upper-case(cbc:ID))='VAT')]/cbc:Percent
)

税率 0 の判定スクリプト訂正

次のルールの判定式が (cbc:Percent = 0)  (xs:decimal(cbc:Percent) = 0)に訂正された。
xs:decimalは、XSLTの関数で対象とするXML要素の値を数値に変換する。

aligned-ibrp-e-06(fatal)
[aligned-ibrp-e-06]-In a Document level allowance (ibg-20) where the Document level allowance tax category code (ibt-95) is “Exempt from tax”, the Document level allowance tax rate (ibt-96) MUST be 0 (zero).
aligned-ibrp-e-07(fatal)
[aligned-ibrp-e-07]-In a Document level charge (ibg-21) where the Document level charge tax category code (ibt-102) is “Exempt from tax”, the Document level charge tax rate (ibt-103) MUST be 0 (zero).
aligned-ibrp-g-06(fatal)
[aligned-ibrp-g-06]-In a Document level allowance (ibg-20) where the Document level allowance tax category code (ibt-95) is “Export” the Document level allowance tax rate (ibt-96) MUST be 0 (zero).
aligned-ibrp-g-07(fatal)
[aligned-ibrp-g-07]-In a Document level charge (ibg-21) where the Document level charge tax category code (ibt-102) is “Export” the Document level charge tax rate (ibt-103) MUST be 0 (zero).

Sharerdルール

新規追加

次の2つのルールが追加された。

ibr-sr-57(fatal)
[ibr-sr-57]-Company identifier MUST exist in the party tax scheme class.
context
//cac:PartyTaxScheme
test
exists(cbc:CompanyID)
ibr-sr-58(fatal)
[ibr-sr-58]-The Invoiced item TAX category code (ibt-151) MUST be present.
context
cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory
test
exists(cbc:ID)

文書レベルのAllowance及びChargeの金額

それぞれ金額を記載しなければならないというルールだが、チェック対象を指定するcontextの記述を変更している。
但し、//cac:AllowanceChargeの記述では、/ubl:Invoice/cac:AllowanceChargeだけでなく/ubl:Invoice/cac:InvoiceLine/cac:AllowanceChargeも対象とされるので、ルールの対象が文書レベル及び明細行レベルと拡張されており意図が正しく反映されていないように思える。

ibr-031(fatal)
[ibr-031]-Each Document level allowance (ibg-20) MUST have a Document level allowance amount (ibt-092).
変更後 context
//cac:AllowanceCharge[cbc:ChargeIndicator = false()]
変更前 context
/ubl:Invoice/cac:AllowanceCharge[cbc:ChargeIndicator = false()] | /cn:CreditNote/cac:AllowanceCharge[cbc:ChargeIndicator = false()]
ibr-036(fatal)
[ibr-036]-Each Document level charge (ibg-21) MUST have a Document level charge amount (ibt-099).
変更後 context
//cac:AllowanceCharge[cbc:ChargeIndicator = true()]
変更前 context
/ubl:Invoice/cac:AllowanceCharge[cbc:ChargeIndicator = true()] | /cn:CreditNote/cac:AllowanceCharge[cbc:ChargeIndicator = true()]

文書レベルのAllowance及びChargeの理由の件数

それぞれ理由件数は最大1 というルールだが、チェック対象を指定するcontextの記述を上記同様に変更している。
こちらもルールの対象が文書レベル及び明細行レベルと拡張されており意図が正しく反映されていないように思える。

ibr-sr-30(fatal)
[ibr-sr-30]-Document level allowance reason MUST occur maximum once
ibr-sr-31(fatal)
[ibr-sr-31]-Document level charge reason MUST occur maximum once

金額の小数点以下の桁数は2以下

Invoice amount due for payment (ibt-115) では、表現を変更しているが内容は前と同じ。

ibr-091(fatal)
[ibr-091]-Invoice amount due for payment (ibt-115) MUST have no more than 2 decimals.
変更後
context
cac:LegalMonetaryTotal
test
string-length(substring-after(cbc:PayableAmount, ‘.’)) <= 2
変更前
context
cac:LegalMonetaryTotal/cbc:PayableAmount
test
string-length(substring-after(., ‘.’)) <= 2

このほかに次の項目について>金額の小数点以下の桁数は2以下という条件が追加された。

ibr-121(fatal)
[ibr-121]-Document level allowance amount (ibt-107) MUST have no more than 2 decimals.
ibr-122(fatal)
[ibr-122]-Document level charge amount (ibt-108) MUST have no more than 2 decimals.
ibr-123(fatal)
[ibr-123]-Invoice total amount without TAX (ibt-109) MUST have no more than 2 decimals.
ibr-124(fatal)
[ibr-124]-Invoice total TAX amount (ibt-110) MUST have no more than 2 decimals.
ibr-125(fatal)
[ibr-125]- Invoice total amount with TAX (ibt-112) MUST have no more than 2 decimals.
ibr-126(fatal)
[ibr-126]- All currencyID attributes must have the same value as the Invoice currency code (ibt-005), except for amounts expected to be in Tax accounting currency (ibt-006).

通貨コード

税務会計通貨コードを使用すると指定されていない金額については請求の文書通貨コードを指定すること。
このルールはBIS Billing 3.0でも定義されていたがPINTでは税務会計通貨コードを使用すると指定された項目が従来の請求書合計税額のほかに拡大されたため対象を指定するcontectが変更された。
対処とする金額のXML要素の階層が不定であることから比較対象の請求の文書通貨コードを//cbc:DocumentCurrencyCodeと // を使用して指定している。

ibr-126(fatal)
[ibr-126]- All currencyID attributes must have the same value as the Invoice currency code (ibt-005), except for amounts expected to be in Tax accounting currency (ibt-006).
context
cbc:Amount | cbc:BaseAmount | cbc:PriceAmount | cbc:LineExtensionAmount | cbc:TaxExclusiveAmount | cbc:TaxInclusiveAmount | cbc:AllowanceTotalAmount | cbc:ChargeTotalAmount | cbc:PrepaidAmount | cbc:PayableRoundingAmount | cbc:PayableAmount |
cac:TaxTotal[cbc:TaxAmount/@currencyID = /*/cbc:DocumentCurrencyCode]/cbc:TaxAmount |
cac:TaxTotal[cbc:TaxAmount/@currencyID = /*/cbc:DocumentCurrencyCode]/cac:TaxSubtotal/cbc:TaxableAmount |
cac:TaxTotal[cbc:TaxAmount/@currencyID = /*/cbc:DocumentCurrencyCode]/cac:TaxSubtotal/cbc:TaxAmount
test
@currencyID = //cbc:DocumentCurrencyCode

数量単位

数量単位には国連コードを使用すること。というルールで従来からあったものだが、cbc:CreditedQuantityが追加された。

ibr-cl-23(fatal)
[ibr-cl-23]-Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension
変更後context

cbc:InvoicedQuantity[@unitCode] | cbc:BaseQuantity[@unitCode] | cbc:CreditedQuantity[@unitCode]
変更前context

cbc:InvoicedQuantity[@unitCode] | cbc:BaseQuantity[@unitCode]

先行する請求書への参照の件数

ルールの文言は変更ないが、context及びtestの記述を変更している。記述表現は違うが定義内容は変わらない。

ibr-sr-06(fatal)
[ibr-sr-06]-Preceding invoice reference MUST occur maximum once
変更後
context
cac:BillingReference
test
(count(cac:InvoiceDocumentReference) <= 1)
変更前
context
/ubl:Invoice | /cn:CreditNote
test
(count(cac:BillingReference/InvoiceDocumentReference) <= 1)

Seller tax representativeの名前の件数

testスクリプトのエラーを訂正。cac:TaxRepresentativeParty/cac:Party/cac:PartyName/cbc:Nameは誤り。正しくは、cac:TaxRepresentativeParty/cac:PartyName/cbc:Name

ibr-sr-22(fatal)
[ibr-sr-22]-Seller tax representative name MUST occur maximum once, if the Seller has a tax representative
context
cac:TaxRepresentativeParty
変更後test
(count(cac:PartyName/cbc:Name) <= 1)
変更前test
(count(cac:Party/cac:PartyName/cbc:Name) <= 1)

ダウンロードファイル解析ソフト(SchematronをTSVに変換)

Python3 ElementTree csvおよび自作のライブラリdic2etreeを使用しています。

#!/usr/bin/env python3
#coding: utf-8
#
# generate TSV from XML Schematron
# 
# designed by SAMBUICHI, Nobuyuki (Sambuichi Professional Engineers Office)
# written by SAMBUICHI, Nobuyuki (Sambuichi Professional Engineers Office)
#
# MIT License
# 
# Copyright (c) 2021 SAMBUICHI Nobuyuki (Sambuichi Professional Engineers Office)
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import xml.etree.ElementTree as ET
from collections import defaultdict
from operator import itemgetter
import csv
import re
import json
import sys 
import os
import argparse

from dic2etree import *

dictID = defaultdict(type(''))
dictXpath = defaultdict(type(''))

def file_path(pathname):
  if '/' == pathname[0:1]:
    return pathname
  else:
    dir = os.path.dirname(__file__)
    new_path = os.path.join(dir, pathname)
    return new_path

def dict_to_tsv(tsv, root):
  def setup_record(context, id, flag, test, text):
    test = test.strip()
    test = ' '.join(test.split())
    text = text.strip()
    text = ' '.join(text.split())
    terms_pattern = 'I?BT-[0-9]*'
    terms = re.findall(terms_pattern, text, flags=re.IGNORECASE)
    terms = ' '.join(terms)
    groups_pattern = 'I?BG-[0-9]*'
    groups = re.findall(groups_pattern, text, flags=re.IGNORECASE)
    groups = ' '.join(groups)
    record = [id, flag, text, context, test, groups, terms]
    return record

  def process_pattern(tsv, pattern):
    pattern_id = pattern['@id']
    rules = pattern['sch:rule']
    if isinstance(rules, dict):
      try:
        context = rules['@context']
        d = rules['sch:assert']
        if isinstance(d, list):
          for v in d:
            record = setup_record(context, v['@id'], v['@flag'], v['@test'], v['#text'])
            tsv.append(record)
        elif isinstance(d, dict):
          record = setup_record(context, d['@id'], d['@flag'], d['@test'], d['#text'])
          tsv.append(record)
        else:
          if verbose:
            print(json.dumps(d))
          pass
      except Exception as expt:
        if verbose:
          print(expt.args)
        pass
    elif isinstance(rules, list):
      for rule in rules:
        try:
          context = rule['@context']
          d = rule['sch:assert']
          if isinstance(d, list):
            for v in d:
              record = setup_record(context, v['@id'], v['@flag'], v['@test'], v['#text'])
              tsv.append(record)
          elif isinstance(d, dict):
            record = setup_record(context, d['@id'], d['@flag'], d['@test'], d['#text'])
            tsv.append(record)
          else:
            if verbose:
              print(json.dumps(d))
            pass
        except Exception as expt:
          if verbose:
            print(expt.args)
          pass
    else:
      pass
    return tsv
  
  # header
  record = ['id', 'flag', 'text', 'context', 'test', 'BG', 'BT']
  tsv.append(record)
  if 'sch:pattern' in root:
    pattern = root['sch:pattern']
    tsv = process_pattern(tsv, pattern)
  elif root['sch:schema'] and isinstance(root['sch:schema'], dict):
    for tag, body in root['sch:schema'].items():
      if 'sch:pattern' == tag:
        for pattern in body:
          tsv = process_pattern(tsv, pattern)

if __name__ == '__main__':
  # Create the parser
  parser = argparse.ArgumentParser(prog='invoice2tsv',
                                  usage='%(prog)s [options] pintFile -o out_file',
                                  description='スキーマトロンファイルをtsvファイルに変換')
  # trn-invoice/schematron/PINT-jurisdiction-aligned-rules.sch -va
  # Add the arguments
  parser.add_argument('pintFile', metavar='pintfile', type=str, help='入力PINT-UBLファイル')
  parser.add_argument('-o', '--out')
  parser.add_argument('-v', '--verbose', action='store_true')
  args = parser.parse_args()
  pint_file = file_path(args.pintFile)
  pre, ext = os.path.splitext(pint_file)
  # tmp_file = pre + '.tmp'
  if args.out:
    out_file = args.out.lstrip()
    out_file = file_path(out_file)
  else:
    out_file = pre + '.txt'
  verbose = args.verbose
  # Check if infile exists
  if not os.path.isfile(pint_file):
    print('入力ファイルがありません')
    sys.exit()
  if verbose:
    print('** START ** ', __file__)

  pint_tree = ET.parse(pint_file)
  pint_root = pint_tree.getroot()
  pint_dict = etree_to_dict(pint_root)
  dicJson = json.dumps(pint_dict)
  dicJson = re.sub('{' + ns[''] + '}', '', dicJson)
  dicJson = re.sub('{' + ns['cac'] + '}', 'cac:', dicJson)
  dicJson = re.sub('{' + ns['cbc'] + '}', 'cbc:', dicJson)
  dicJson = re.sub('{' + ns['sch'] + '}', 'sch:', dicJson)
  pint_dict2 = json.loads(dicJson)
  pint_tsv = []
  dict_to_tsv(pint_tsv, pint_dict2)

  pint_set = set([json.dumps(x) for x in pint_tsv])
  pint_list = list(pint_set)
  tsv = [json.loads(w) for w in pint_list]
  sorted_tsv = sorted(tsv,key=lambda x: x[0])

  header = ['id', 'flag', 'text', 'context', 'test', 'BG', 'BT']

  with open(out_file, 'w') as f:
      writer = csv.writer(f, delimiter='\t')
      writer.writerow(header)
      for d in sorted_tsv:
        writer.writerow(d)

  if verbose:
    print(f'** END ** {out_file}')