にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

このブログについて

文字・フォント・プログラム・技術・趣味などについて、Twitterでは書きづらい長い内容などをまとめるためのブログです。基本的には自分用のメモとして書いている部分が多いです。

リンク等

note https://note.com/nixeneko 残らなくてもいい記事
Pixiv https://pixiv.me/nixeneko
Tumblr https://nixeneko.tumblr.com/ 絵。Pixivと同じにしたいがサボリ気味
Pleroma @nixeneko@nixeneko.info Mastodonとかやってる人はフォローしてください
Twitter @nixeneko
GitHub https://github.com/nixeneko プログラムとか
Bookmeter https://bookmeter.com/users/9166 読書記録。たまに感想
D4DJグルミク oDgpcYR9
Amazon欲しい物リスト amazon.jp/registry/wishlist/1C43ZFBA4IL6Z

同人誌(無料公開)

https://nixeneko.hatenablog.com/entry/c88_russian_alphabethttps://nixeneko.hatenablog.com/entry/c90_greek_latin_cyrillichttp://nixeneko.hatenablog.com/entry/20170811_dentyu

Epicyon建ててみたメモ

AcrivityPubの一実装であるEpicyonサーバーを立ててみた。

Epicyon

EpicyonはActivityPubという規格に基づいて通信を行うSNSソフトウェアである。
ActivityPubを話すソフトウェアは互いに通信することができ、Mastodonなどと相互に通信ができる。

Epicyonは次のページにある。
https://epicyon.net/

特徴としては、JavaScriptを使っていないので、ページの動的な更新がされない、またリレーショナルデータベースなどを利用せず、データは普通のファイルとして保存されるそうだ。サーバーはPythonで書かれている。

インストールメモ

サーバーのセットアップ

VPS用意

WebARENA IndigoでUbuntu 22.04、メモリ1GBのインスタンス作成

DNSレコードを設定

ドメイン(以降の説明ではepicyon.example.comとする)にIPアドレスを設定。

ログイン

sshでログイン

とりあえず更新
sudo apt update
sudo apt upgrade
sudo reboot now

再ログイン

sshd設定
sudo nano /etc/ssh/sshd_config

次のような設定を行った。

port 10022 

LoginGraceTime 30
PermitRootLogin no
MaxAuthTries 3
MaxSessions 3

再起動

sudo sshd -t
sudo service sshd restart

ファイアウォールとかの設定は略。

Epicyonのインストール

諸々入れる
sudo apt install python3-socks imagemagick python3-setuptools python3-cryptography python3-dateutil python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx wget

インストールの説明だとtorもインストールすることになってるけど省いた。

ソースコードとってくる
cd /opt
sudo git clone https://gitlab.com/bashrc2/epicyon
ユーザーの追加
sudo useradd -r -s /bin/false -d /opt/epicyon -U epicyon
sudo chown -R epicyon:epicyon /opt/epicyon

なんかリンク張る。以降ドメインepicyon.example.comとする。

sudo mkdir /var/www/epicyon.example.com
sudo mkdir -p /opt/epicyon/accounts/newsmirror
sudo ln -s /opt/epicyon/accounts/newsmirror /var/www/epicyon.example.com
sudo chown -R epicyon:epicyon /opt/epicyon/accounts/

accounts/以下のownerがrootになっていたので変更した。

デーモンを作成
sudo nano /etc/systemd/system/epicyon.service

公式ページの説明にあるように次のような内容を貼り付ける

[Unit]
Description=epicyon
After=syslog.target
After=network.target

[Service]
Type=simple
User=epicyon
Group=epicyon
WorkingDirectory=/opt/epicyon
ExecStart=/usr/bin/python3 /opt/epicyon/epicyon.py --port 443 --proxy 7156 --domain epicyon.example.com -registration open --log_login_failures
Environment=USER=epicyon
Environment=PYTHONUNBUFFERED=true
Restart=always
StandardError=syslog
CPUQuota=80%
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
ProtectHostname=true
ProtectClock=true
ProtectProc=invisible
ProcSubset=pid
PrivateTmp=true
PrivateUsers=true
PrivateDevices=true
PrivateIPC=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictNamespaces=true
SystemCallArchitectures=native

[Install]
WantedBy=multi-user.target

YOUR_DOMAINだけ使うドメイン(ここでは仮にepicyon.example.com)に置換している。
ユーザー登録を無効化するにはExecStartのところの--registration open--registration closedに変更するといいっぽい。

有効化

sudo systemctl enable epicyon
sudo systemctl start epicyon
Webサーバーの設定
sudo nano /etc/nginx/sites-available/epicyon.example.com

公式の説明通りに次のような内容を貼り付ける。

server {
 listen 80;
 listen [::]:80;
 server_name epicyon.example.com;
 access_log /dev/null;
 error_log /dev/null;
 client_max_body_size 31m;
 client_body_buffer_size 128k;
 
 limit_conn conn_limit_per_ip 10;
 limit_req zone=req_limit_per_ip burst=10 nodelay;
 
 index index.html;
 rewrite ^ https://$server_name$request_uri? permanent;
 }
 
 server {
 listen 443 ssl;
 server_name epicyon.example.com;
 
 gzip on;
 gzip_disable "msie6";
 gzip_vary on;
 gzip_proxied any;
 gzip_min_length 1024;
 gzip_comp_level 6;
 gzip_buffers 16 8k;
 gzip_http_version 1.1;
 gzip_types text/plain text/css text/vcard text/vcard+xml application/json application/ld+json application/javascript text/xml application/xml application/rdf+xml application/xml+rss text/javascript;
 
 ssl_stapling off;
 ssl_stapling_verify off;
 #ssl on;
 ssl_certificate /etc/letsencrypt/live/epicyon.example.com/fullchain.pem;
 ssl_certificate_key /etc/letsencrypt/live/epicyon.example.com/privkey.pem;
 #ssl_dhparam /etc/ssl/certs/epicyon.example.com.dhparam;
 
 ssl_protocols TLSv1.2 TLSv1.3;
 ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
 ssl_prefer_server_ciphers on;
 ssl_session_cache shared:SSL:10m;
 ssl_session_tickets off;
 
 add_header Content-Security-Policy "default-src https:; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'";
 add_header X-Frame-Options DENY;
 add_header X-Content-Type-Options nosniff;
 add_header X-XSS-Protection "1; mode=block";
 add_header X-Download-Options noopen;
 add_header X-Permitted-Cross-Domain-Policies none;
 add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always;
 
 access_log /dev/null;
 error_log /dev/null;
 
 index index.html;
 
 location /newsmirror {
 root /var/www/YOUR_DOMAIN;
 try_files $uri =404;
 }
 
 keepalive_timeout 70;
 sendfile on;
 
 location / {
 proxy_http_version 1.1;
 client_max_body_size 31M;
 proxy_set_header Host $http_host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forward-Proto http;
 proxy_set_header X-Nginx-Proxy true;
 proxy_temp_file_write_size 64k;
 proxy_connect_timeout 10080s;
 proxy_send_timeout 10080;
 proxy_read_timeout 10080;
 proxy_buffer_size 64k;
 proxy_buffers 16 32k;
 proxy_busy_buffers_size 64k;
 proxy_redirect off;
 proxy_request_buffering off;
 proxy_buffering off;
 proxy_pass http://localhost:7156;
 tcp_nodelay on;
 }
 } 

YOUR_DOMAINを使うドメインepicyon.example.com)に置換している。
ssl on;はwarning出たのでコメントアウトした。

これだけだとエラーでて動かないので、Nginxの設定を編集し

sudo nano /etc/nginx/nginx.conf

で、httpセクション内、includeの前に

	limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
	limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=50r/s;

とかを追加。

有効化

sudo ln -s /etc/nginx/sites-available/epicyon.example.com /etc/nginx/sites-enabled/
SSH対応
sudo systemctl stop nginx
sudo certbot certonly -n --server https://acme-v02.api.letsencrypt.org/directory --standalone -d epicyon.example.com --renew-by-default --agree-tos --email test@example.com
sudo systemctl start nginx

メールアドレスは適切に設定。


これで動き出すっぽい。
ダメな場合はNginxについてはsudo systemctl status nginx.servicesudo journalctl -xeu nginx.serviceなどで、Epicyonについてはsudo journalctl -f -u epicyonとかでログが見られるっぽい。

使う

これでブラウザからアクセスすると登録画面が出てきて使うことができる。

感想

WebクライアントのUIがこなれてない。デザインもあんまり分かりやすくないし、見た目もあんまりよくない気がする。

Mastodonとは通信ができたが、自分のメインのPleromaインスタンス(nixeneko.info)とはうまく疎通できなかった。Pleroma→Epicyonは配送できてもEpicyon→Pleromaは配送できなかった。Pleromaバージョンを上げればいいのかもしれないが…。

結局、試すには試したけど常用するには難しいかなという感じ。


さすがにWebクライアントが最新投稿を自動で取ってくるくらいはして欲しい気がする。とはいえ適当なアプリ使えばいいのかも。

自由ソフトウェア的な思想が結構強い雰囲気を感じる。好みは分かれるかもしれない。

特に、コミュニケーションを目的とせず、情報を配信するために自分でActivityPubサーバーを立てる時にはこういった選択肢は有用であると思われる。動かすのに強いマシンが要らないので。

ログインしてるのに、はてなブログの自分のページがログイン状態にならない

概要

  • Firefoxのアップデートにより、デフォルトでクロスサイトCookieが隔離されるようになった。
  • これによりはてなブログのページからはてなのログイン情報を取得できなくなり、ログインしていない表示になった。
  • FirefoxCookieブロックの設定を変更することで、問題は解消できる。

現象

Firefoxで、しばらく前から、はてなブログの自分のブログを開いても、自分のアカウント名が表示されず、「ログイン」というリンクの表示になっていた。

自分のはてなブログを開いてもログイン状態にならない

また一方で、「ログイン」をクリックすると、ログインされた状態で自分のダッシュボードに移動する。つまり、はてなブログにログインはしている状態である。

一度ログアウトして再度ログインし直したものの、改善は見られなかった。


これは、以前は次のように表示されていたものである。

ログイン状態でユーザーアカウントが正常に表示された場合

環境

解決方法

方法1: FirefoxのクロスサイトCookieの隔離をやめる

  1. Firefoxの「ツール(T)」「設定(S)」を開く。
  2. 「プライバシーとセキュリティ」タブを開く。
  3. 「強化型トラッキング防止機能」について、「カスタム(C)」のラジオボタンを選択。さらに、「Cookie (C)」の右側のドロップダウンボックスを選び「クロスサイトトラッキング Cookie」を選択する。
  4. 設定タブを閉じ、はてなブログのページを再読み込みする。
FirefoxでクロスサイトCookieを許可する設定

セキュリティ上の懸念が多少あるかもしれないものの、すべてのはてなブログのページについて当問題を一括で解決できる。

方法2: 個別サイトについて、強化型トラッキング防止機能をオフにするよう例外に追加する

  1. Firefoxで当該サイトを開く。
  2. アドレスバー左の盾マークをクリックする
  3. 「強化型トラッキング防止機能はこのサイトでオンです」の右のスイッチをクリックし、オフにする
強化型トラッキング防止機能をオフにする手順

次の図のような状態になると正常に表示される。

強化型トラッキング防止機能をオフにした状態

この方法のメリットとしては、設定したサイト以外のセキュリティは保たれるということがあるが、デメリットはサイトごとに個別に設定する必要があるということだ。はてなブログが複数ある場合、それら全てで設定を行う必要がある。

原因

Firefoxが、最近(2022年6月?)のアップデートで、デフォルトでクロスサイトCookieを隔離するように変更されたらしい。なので、自分のブログページはhatena.ne.jpドメインではないので、はてなブログのページからははてなのログイン情報が取得できなくなり、そのためログインがなされていないような表示になったのだと思われる。

これはSafariなどでは以前から発生していた状況らしい(参考サイト1参照)。

Private Reserve Inkの今

Private Reserve社のインク

Private Reserve社という、万年筆インクを製造している会社があった。
その会社が製造するインクは発色がよく、染料インクなので扱いやすく、混色も可能というものだった。
そのため、自分で好みの色のインクを調合できる店であるカキモリのInkstandにおいて、混色用のインクとして2017年頃まで使われていた。しかし、安定した入手が困難になったため、現在は独自の顔料系インクに切り替わっている。

入手困難だった理由

なぜインクが入手困難になったのか?

Private Reserve Ink - Status? - Inky Thoughts - The Fountain Pen Network
このフォーラムを追っていくと経緯が書いてある。フォーラムの記載によると、次のようなことがあったらしい。

まず、元々の会社のオーナーが逝去したとのことで、そのためインクが生産できなくなった。新しく生産されないため、在庫がなくなると入手困難状態になっていた。

その後、従業員であったDarla Aniline氏が2018年にPrivate Reserve社を購入し、一人会社として運営されていた。インク調合のレシピがほとんど残されていなかったので再生産が難しかったという事情もあり、配合を再現しようと化学者と共同して作業していたらしい。しかし、2020年9月にAniline氏も早逝。会社は機能停止したことになる。

新しいPrivate Reserve Ink

さて少し経って、Cult Pensという文具ショップのFacebook投稿によると、2021年3月に元々のものと同じレシピ(the original formula)で新しく製造・販売が開始されたとのこと。


私が気づいたときにはCult Pensで在庫切れになっいたが、その後在庫が復活したので、2021年8月にインクを購入した。
インクの瓶を箱から出して並べた写真

箱の底面
箱の側面
箱には製造者については書かれていないが、ヨーロッパで製造、アメリカで包装とあり、また www.PrivateReserveInkUSA.com というアドレスが書かれている。これにアクセスすると yafabrands.com のページにリダイレクトされる。これはYafaというアメリカの筆記具会社らしい。


探すとYafa社がPrivate Reserve社を買収したと書かれているPDFがあった。
https://www.yafa.com/outlet/images/Pen_world_article.pdf

というわけで、2021年3月ころからYafaブランドの元で新しく製造されたPrivate Reserveインクの流通が始まったらしいことが分かった。当分の間は安定した供給が見込めそうである。とりあえずは一安心という感じだ。

購入方法

日本からも、CultPensなどの日本へも配送してくれるネットショップで購入できる。しかし、私が2021年8月に購入した時は3つのボトルを購入して送料が1500円程度(恐らく14~15 USD)だったものの、現在購入しようとしたら安い運送方法の選択肢がなくなって5800円程度(たぶん50 USD)はかかるようで、躊躇している。

Ebayとかで購入する方が配送料が安い(3千円以内)っぽいので、今はそっちで購入するのがいいのかもしれない。もうちょっと円が強いときに買いたいが…。


ところで、私が混色に使っていたFoam Greenというインクを含む数色が、新体制では製造されなくなってしまったらしく、手元で今までと同じ色のインクを調合することが難しくなってしまった。別のインクを使って近い色にできるように試行錯誤しなければならない。まあそれは追い追い。

おまけ

This is a pencil.と書かれた鉛筆
Cult Pensでインクを購入した時、“This is a pencil.”と書かれた鉛筆がおまけに付いてきた。すき。
This is a pencilと書かれた鉛筆の拡大

Amazon Timestreamからboto3で一度に大量のデータを取得しようとしたが空データが返ってきた

問題の概要

AWSAmazon Timestreamというデータベースにセンサーデータを蓄積している。このデータをローカルにダウンロードして利用したい。
Pythonとboto3ライブラリを利用して、Timestreamのデータベースからそこそこ大量のデータを取得しようとしたところ、レスポンスのRowsが空だった。
データがある程度小さくなるクエリでやってみると、問題なくデータが取得できるのが確認できた。

原因

TimestreamQuety.Client.query()を呼び出す際に、MaxRowsキーワード引数を指定しないと、取得結果のサイズが1MB以上になる場合に、Rowsが空の状態でレスポンスが返ってくる。その代わり、NextTokenがレスポンスに含まれる。

Otherwise, the initial invocation of Query only returns a NextToken , which can then be used in subsequent calls to fetch the result set. To resume pagination, provide the NextToken value in the subsequent command.

TimestreamQuery — Boto3 Docs 1.21.37 documentation

要するに、取得するデータがでかいと1回で取得することができないようになっている。

対応

ページネーションする。

TimestreamQuety.Client.query()NextTokenを指定して呼び出すと、サイズが1MB未満になるような個数のデータ(Rows)が返される。
更にまだ未取得のデータがある場合には、次のデータに対応するNextTokenもレスポンスに含まれる。データをすべて取得してしまった場合には、レスポンスにはNextTokenが存在しないので、終了判定にも使える。


実際にはこういう手順になる。

  1. 取得したいデータが1MB以上になる場合、最初に呼び出すとNextTokenと空のRowsが得られる。
  2. このNextTokenを利用して1MB程度分のデータ(Rows)と次のデータに対応するNextTokenを得る。
  3. これをNextTokenがレスポンスに含まれなくなるまで続け、今までに返されたRowsをすべて合体させれば欲しいデータが得られる。

Python 3コード

Rowsだけ連結して返すみたいなPython 3コードを示す。

import json
import boto3
from botocore.config import Config

#リージョン名, ID, secret keyは省略
config = Config(region_name = '……') 
config.endpoint_discovery_enabled = True
timestream_query_client = boto3.client('timestream-query', 
        aws_access_key_id="……",
        aws_secret_access_key="……",
        config=config)

#クエリの例
QUERY = """SELECT "time", "measure_name", "measure_value::double" FROM "mydatabase"."mytable" AND time between '2021-01-01 00:00' and '2022-01-01 00:00' ORDER BY time ASC""" 

#データ取得用の関数
def getdata(next_token = None):
    if next_token: #next_tokenが指定された場合はNextTokenを指定してクエリ
        result = timestream_query_client.query(
            QueryString=QUERY,
            NextToken=next_token
        )
    else: #next_tokenなし=初回呼び出し
        result = timestream_query_client.query(
            QueryString=QUERY
        )
    
    ret_rows = result["Rows"] #list of data
    if "NextToken" in result: #次のページ(未取得データ)が存在する場合
        return ret_rows + getdata(result["NextToken"]) #再帰呼び出し
    else: #欲しいデータはすべて取得した場合
        return ret_rows 

rows = getdata()
print(len(rows))
print(rows[0])
問題点

途中経過を保存していないので、ダウンロード途中で失敗した際に、やり直すときには最初から取得しなおしになる。
かなりたくさんのデータを取ってくる必要があるのであれば、呼び出しの度に毎回(あるいは何回かに一度)データとNextTokenを保存するなどとして、レジュームができるようにした方がよさそう。

また、再帰呼び出ししてるけど末尾再帰になってないので再帰深くなると大変かもしれない。メモリも食うし。普通にループで処理した方が適してるかも。

Python 3のElementTreeでXMLを解析する

PythonのElementTreeを使ってXMLを解析するときに少し悩んだので、忘れないようにメモ。
もっと例を洗練させるべきだとは思うが、後回しにする。

インポートとファイルの読み込み

import xml.etree.ElementTree as ET
tree = ET.parse('country_data.xml')
root = tree.getroot()

一方、ET.fromstring()メソッドで作成する場合、返されるのはルート要素となる。

タグを取り出す

以下、elemは何らかのタグに対応する要素とする(上のrootなど)

再帰

.iter()メソッドは、深いものについても順番に見ていくイテレータを返す。

for target_tag in elem.iter("target"):
    #...

とか。

直下で最初にみつけたもの1つ

.find()メソッドは直下のもののみ、最初に一致した要素一つだけを返す。

target_tag = elem.find("target")

これは後述するXPath記法と組み合わせることで格段に便利になる。

マッチしない場合はNoneになる。ただし、

if target_tag: #この書き方では動かない
    #マッチした場合

みたいには書けない(マッチした場合でも実行されない)。なので、

if target_tag is not None:
    #マッチした場合

みたいに書いたら動いた。

直下でマッチするものすべて

.findall()メソッドは直下のもののみ、一致した要素をすべて含むリストを返す。

for target_tag in elem.find("target"):
    #...
添え字でのアクセス

内側のタグには添え字でもアクセスできる。直下で最初くるタグのそのまた直下の最初に来るタグだと次のように書ける。

elem[0][0]

XPath記法

これを使えると格段に解析効率が上がる

今の階層以下の(直下とは限らない)タグを検索

//という、子要素やその子孫の要素すべてを対象とする記法が使える。

target = elem.find(".//target")
階層構造を指定
<a><b><c>こんにちは</c></b></a>
element = ET.fromstring("<element><a><b><c>こんにちは</c></b></a></element>")
target_c = element.find("a/b/c")
属性を指定
target = elem.find("target[@attrib='value']")

属性が"'を含む場合、エスケープするなり引用符を変更するなどすれば動く。

element = ET.fromstring('''<root><target name="Do's and dont's">some text</target></root>''')
target = element.find('''target[@name="Do's and dont's"]''')
2つ以上の属性を指定
target = elem.find("target[@attrib1='value 1'][@attrib2='value 2']")

属性の取り出し

element = ET.fromstring('<greetings myvalue="hello world"/>')
element.get("myvalue") #"hello world"

テキスト取り出し

<a><b>いz<c></c></b><d></d></a>

みたいな構造があった場合、
a.textは「あ」になる

a = ET.fromstring("<a>あ<b>い<c>う</c></b><d>え</d>お</a>")
print(a.text) #"あ"

ここで、「あいうえお」が欲しければ、

print( "".join(a.itertext()) ) #あいうえお

のようにするとよい。.itertext()で現在の要素以下のテキストを順番に返すイテレータが得られるので、それを合体させている。

2022年電子帳簿保存法に対応した、通販の領収書PDF類の保存法を考える

注意: 素人が調べたことなので間違い等を含む可能性があります。専門家の意見を聞くなり自分で原文にあたるなりして判断してください。

改定された「電子帳簿保存法」が2022年1月1日から施行され、通販などで領収書等をWebサイトで出力するものについて、プリントアウトして保存しておくことは認められなくなった*1

…となるはずだったが、2023年12月31日までの取引は、経過措置として、電子保存できないことについて「所轄税務署長がやむを得ない事情があると認め」た場合にはプリントアウトで保管してもよいということになったらしい*2
やむを得ない事情があると認めた場合云々といっても、特に申請や手続きは必要ない*3ため、来年末まではプリントアウトして保存しておいても問題なさそうではある。


とはいえ、2年のうちに準備しておかなければならない。

会計ソフトやクラウドサービスを利用している場合は、電子領収書等のデータを保存するための仕組みが多分つくられているだろうから、それを利用するのが簡単だと思われる。

そういうのを使わない場合はどうするか。


凡例: 鍵括弧「~」は用語や引用を表す。

電子取引の電子保存の要件

「電子取引」とは、取引情報(注文書、契約書、送り状、領収書、見積書等の情報)をインターネットや電子メール、EDI取引等でやりとりすることを指すとのこと*4
通販のWebサイトで出力した領収書PDFは電子取引の取引情報に該当するわけなので、電子的に保存する必要がある。


電子データとして保存する場合、次のような方法が認められる*5。以下に引用する。

  • 1 電子メールに請求書等が添付された場合
    • ⑴ 請求書等が添付された電子メールそのもの(電子メール本文に取引情報が記載されたものを含みます。)をサーバ等(運用委託しているものを含みます。以下同じです。)自社システムに保存する。
    • ⑵ 添付された請求書等をサーバ等に保存する。
  • 2 発行者のウェブサイトで領収書等をダウンロードする場合
    • ⑴ PDF等をダウンロードできる場合
      • ① ウェブサイトに領収書等を保存する。
      • ② ウェブサイトから領収書等をダウンロードしてサーバ等に保存する。
    • ⑵ HTMLデータで表示される場合
      • ① ウェブサイト上に領収書を保存する。
      • ② ウェブサイト上に表示される領収書をスクリーンショットし、サーバ等に保存する。
      • ③ ウェブサイト上に表示されたHTMLデータを領収書の形式に変換(PDF等)し、サーバ等に保存する。
  • 3 第三者等が管理するクラウドサービスを利用し領収書等を授受する場合
    • クラウドサービスに領収書等を保存する。
    • クラウドサービスから領収書等をダウンロードして、サーバ等に保存する。
  • 4 従業員がスマートフォン等のアプリを利用して、経費を立て替えた場合
    • 従業員のスマートフォン等に表示される領収書データを電子メールにより送信させて、自社システムに保存する。
    • なお、この場合にはいわゆるスクリーンショットによる領収書の画像データでも構いません。

引用終わり。基本的にPDFか、PDFが難しければスクリーンショット画像として保存するようである。



「電子取引の取引情報に係る電磁的記録の保存等を行う場合」には、次の要件を満たす必要があるらしい*6

  • 「電子計算機処理システムの概要を記載した書類の備付け(自社開発のプログラムを使用する場合に限ります。)」
  • 「見読可能装置の備付け等」
  • 「検索機能の確保」
  • 改竄防止措置

このうち、「電子計算機処理システムの概要を記載した書類の備付け」はまあ自分でプログラム書かなければ関係なさそう。

「見読可能装置の備付け等」は、電子取引の取引情報にアクセスできるパソコンを用意しておいて、税務調査のときにそれを利用してすぐに見せることができるようにしておけばいいらしい*7

あとは「検索機能の確保」と改竄防止措置である。

検索機能の確保

検索機能の確保については次の要件を満たす必要があるとのこと*8

  1. 取引年月日(などの日付)・取引金額・取引先を検索条件にできる
  2. 日付と金額は範囲を指定して検索できる
  3. 2つ以上の項目をAND検索できる

これは、次のような方法で対応できる。

規則的なファイル名を付けることによる対応

「2022年(令和4年)10月31日に株式会社国税商事から受領した110,000円の請求書」の場合であれば、

20221031_㈱国税商事_110000

などと、日付、取引先名、金額を含む規則的なファイル名をつけておくとよいとのこと*9

このような規則的な名づけを行うことで、ファイラーを使って検索ができ、検索要件を満たすことができるようだ。*10

しかしこのままでは範囲検索はできない。範囲検索がなくても、税務職員の求めに応じて提出できるようにしておけば、検索要件を満たすとしてよいらしい*11

また、検索要件は年商1000万円以下の場合は免除されるっぽい*12ので、該当する場合はこの名前付け自体必須ではないようだ。ただし必要に応じて提出できるようにしておく必要はあるはずだが。

範囲検索がしたければ、Pythonスクリプトなどを書いて、範囲検索を含んだ検索機能を実装するという手はある。

Excelを使った場合

もう一つの手としてはExcelを使うことがある。
Excelファイルに、領収書等のファイル名、取引の発生した日付、取引先、金額をリストとして入力しておけば、Excelの機能を利用して範囲検索やAND検索が可能である*13

改竄防止措置

「電子的に受け取った請求書や領収書等」の「真実性を確保する観点から、以下のいずれかの条件を満たす必要があ」るとのこと*14。以下引用する。

  1. タイムスタンプが付与されたデータを受領
  2. 速やかに(又はその業務の処理に係る通常の期間を経過した後、速やかに)タイムスタンプを付与
  3. データの訂正削除を行った場合にその記録が残るシステム又は訂正削除ができないシステムを利用
  4. 訂正削除の防止に関する事務処理規程を策定、運用、備付け

引用終わり。


さて、改竄等を防ぐための措置が必要であるが、クラウド会計サービスなどを使うのでなければ個人で改竄防止システムを用意するのは難しい。Gitだって歴史を改変できるので。

これは4.に従って、「電子取引データの訂正及び削除の防止に関する事務処理規程」を備え付けることで(そしてそれに従って管理することで)いいらしい*15
規程のサンプルは次のページからダウンロードできる。

まとめ

以下のような感じでデータ保存を行えば問題ないと思われる。

  • 基本的にPDFで保存する。PDFが難しい場合はスクリーンショット画像で保存する
  • いつでもデータを見られるパソコンを用意しておく
  • 日付・取引先名・取引金額を含んだ、規則的なファイル名をつけ、検索しやすいように場所を決めて保存する
  • 「電子取引データの訂正及び削除の防止に関する事務処理規程」を備え付ける

私は専門家ではないので正しさの保証はできません。最終的には参考の欄に挙げたPDFを自分で読んだり、専門家の意見を聞いて判断してください。

*1:所得税源泉徴収に係る所得税を除きます。)及び法人税の保存義務者が取引情報(注文書、領収書等に通常記載される事項)を電磁的方式により授受する取引(電子取引)を行った場合には、その取引情報を電磁的記録により保存しなければならない」(『電子帳簿保存法一問一答 【電子取引関係】』問1)

*2:電子帳簿保存法一問一答 【電子取引関係】』問41-2~問41-3

*3:電子帳簿保存法一問一答 【電子取引関係】』問41-4

*4:電子帳簿保存法一問一答 【電子取引関係】』問2

*5:電子帳簿保存法一問一答 【電子取引関係】』問27

*6:電子帳簿保存法一問一答 【電子取引関係】』問11

*7:電子帳簿保存法一問一答 【電子取引関係】』問13

*8:電子帳簿保存法一問一答 【電子取引関係】』問31

*9:電子帳簿保存法一問一答 【電子取引関係】』問12

*10:電子帳簿保存法一問一答 【電子取引関係】』問33

*11:「当該電磁的記録について、税務職員による質問検査権に基づくダウンロードの求めに応じることができるようにしている場合には、この項目を組み合わせて条件を設定できる機能(及び範囲を指定して条件を設定できる機能)は不要となります」(『電子帳簿保存法一問一答 【電子取引関係】』問32)

*12:電子帳簿保存法一問一答 【電子取引関係】』問34

*13:電子帳簿保存法一問一答 【電子取引関係】』問33

*14:電子帳簿保存法一問一答 【電子取引関係】』問22

*15:電子帳簿保存法一問一答 【電子取引関係】』問22~24

自営業でも適格請求書を発行したい: 請求書簡易作成システムの構成

概要

登録番号さえ用意して追加すれば適格請求書を作成できる(と思われる)システムを、Microsoft Excel, Microsoft Word, Pythonを利用して構成した。
データ入力をExcelで行い、Pythonスクリプトで形を変換し、そのデータを元にWordで差し込み印刷を行うことにより請求書を得る仕組みを作った。

作成したファイルやスクリプトは次のURLに置いた:
GitHub - nixeneko/seikyusho_sample: Microsoft Word, ExcelとPythonを使って請求書を作るサンプル

はじめに

インボイス制度とかいうものが始まるらしい、なんかやらないといけないらしい。

制度に関する説明は次のサイトがわかりやすい。
www.itmedia.co.jp


曰く、2023年10月から「適格請求書」というのを発行しないと、(得意先における)消費税の控除関係で不利になるらしい。なので「適格請求書」を必要になったら発行できるように準備しておきたい。

「適格請求書」の要件

請求書に次の内容を含めないといけないらしい。

  1. 書類作成者の氏名又は名称
  2. 取引年月日
  3. 取引内容(軽減税率の対象品目である旨)
  4. 税率ごとに区分して合計した税込対価の額
  5. 書類の交付を受ける事業者の氏名又は名称
  6. 登録番号
  7. 適用税率及び税率ごとに区分した消費税額等

参照:


現行の「区分記載請求書」では、6.と7.が要らないということで、この分が増えることになる。なお、6.の登録番号の発行は申請が必要となっている。とりあえず、6.は2023年10月から必要になるので、それまでは、いつでも追加できるようにしておいて、登録番号の表記を入れれば適格請求書となる状態をつくりたい。

なお、この記事では登録番号の発行を受けるための申請については扱わないこととする。


使うソフト

さて、請求書を発行するといっても、そのために専用のソフトを新しく買ったりとかはしたくない。

Microsoft WordとExcelを使うと差し込み印刷ができるらしいので、Microsoft Officeを基本として使うことにする。事務処理でMicrosoft Officeは使っていることも多いと思うので。

差し込み印刷

Wordには、Excelなどのデータを読み込んで文面を差し替える機能が備わっていて、「差し込み印刷」という。

差し込み印刷については次のページなどが参考になる:
差し込み印刷で請求書のように複数レコードや合計を表示したい:Word(ワード)2013基本講座


以下、Microsoft OfficeのバージョンはMicrosoft 365 MSO (バージョン 2112 ビルド 16.0.14729.20156) 64 ビットを使っているが、基本的に別のバージョンでも問題ないと思われる。

差し込み印刷のフォーマットをつくる

Wordでひな形をつくる
f:id:nixeneko:20220107151249p:plain
請求書ひな形

こんな感じのを作った。

税込みベースの作業フローを使っていたのでそのまま税込みベースで計算しているが、統一されていれば税別ベースで計算してもよい*1

Excelで、差し込めるデータ構造を決める

サンプルデータを用意した。こんな感じのものであれば差し込み印刷で問題なく使える。シートの名前は「請求書」とした。

f:id:nixeneko:20220107232007p:plain
Excelサンプルデータ

必要なのはテキストや数値のみであって、幅やフォーマットは関係ない。
グレーの部分はデータとしては利用していないので、空欄や別の値でも問題ない。

Wordに差し込み印刷を設定する

[差し込み文書]タブを開く→[差し込み印刷の開始]→[標準のWord文書]をクリック

[宛先の選択]→[既存のリストを使用(E)...]をクリック

先ほどサンプルデータを用意したエクセルファイルを選んで、[開く(O)]を押す。

[テーブルの選択]ウィンドウが出るので、データのあるシート(ここでは「請求書$」)を選択して[OK]を押す。
([先頭行をタイトル行として使用する(R)]にチェックが入っていることを確かめる。)

___様となっているところにカーソルを移動して、[差し込みフィールドの挿入]→「被請求者」を選択すると«被請求者»と書かれたものが挿入される。

同様に年月日のところに「年」「月」「日」の差し込みフィールドを挿入する。


注文内容の部分には工夫が必要である。
1行目は次のように差し込みフィールドを挿入する。

  • 品目→「品目」
  • 税込単価→「単価」
  • 個数→「個数」
  • 小計→「品目小計」

ここで、挿入された«単価»を右クリックし、[フィールド コードの表示/非表示(T)]をクリック。
すると{ MERGEFIELD 単価 }というような表示になるので、閉じ曲括弧}の直前に\# ¥,0 を入力して次のようにする。

{ MERGEFIELD 単価 \# ¥,0 }

これを再度右クリックして[フィールド コードの表示/非表示(T)]をクリック。

追加したコードの意味は次のようになっている。

  • \# はnumeric picture switchというWordの機能らしく、数値の表示形態を指定する。
  • ¥は円記号(U+00A5)である。
    • 円記号¥(U+00A5)の代わりにバックスラッシュ\(U+005C)を用いる場合は、\\としなければならない。ただ、最新のコンピュータではバックスラッシュが円記号で表示されないことも多くなってきているし、円記号の方を使っておけばいいと思う。昔のシステムではわからないが…
  • ,は3桁ごとにコンマ区切りをすることを表す。
  • 0は整数で、1桁の場合でも先頭を0パディングしないことを表す。

参考: Fix the Formatting of an Excel Mail Merge Field in a Word Document


同様に«品目小計»のフォーマットも指定する。


2行目の品目の欄に移動し、[ルール]→[Next Record If (条件により次のレコード)(X)...]をクリック。

[Wordフィールドの挿入: Next Record If]ウィンドウが表示されるので、

  • [フィールド名(F)] →「請求書ID」
  • [比較(C)] → [空白でない]

にして[OK]をクリックすると«Next Record If»と挿入される。

その直後に「品目」という差し込みフィールドを挿入する。
2行目の他のセル、税込単価, 個数, 小計のところは1行目と同じでよい。

Next Record Ifというのがなぜ必要かというと、これがないと一つの請求書で1つの行しか参照せず、同じ品目が並ぶことになる。そのため、「次の行に進んでください」とWordに指示を出すコマンドが必要で、それがNext Record Ifである。ここでは「請求書ID」が空白でない場合のみ次の行に移るように設定したので、空白行より先には進まないでそこで止まる。


3行目以降は2行目と同じてよく、2行目をコピペすればよい。


また、他のものについても同様に差し込みフィールドを挿入し、フィールドコードを編集することで円記号前置+コンマ区切りを指定する。

  • ご請求金額→「代金計」{ MERGEFIELD 代金計 \# ,0 }
  • 合計金額→「代金計」{ MERGEFIELD 代金計 \# ¥,0 }
  • 税率10%対象金額(税込)→「標準税率対象」{ MERGEFIELD 標準税率対象 \# ¥,0 }
  • 10%税額→「標準税」{ MERGEFIELD 標準税 \# ¥,0 }
  • 税率8%対象金額(税込)→「軽減税率対象」{ MERGEFIELD 軽減税率対象 \# ¥,0 }
  • 8%税額→「軽減税」{ MERGEFIELD 軽減税 \# ¥,0 }
  • 消費税計→「消費税計」{ MERGEFIELD 消費税計 \# ¥,0 }


出来上がったものは次のような見た目になる。

f:id:nixeneko:20220107155810p:plain
差し込みフィールドの挿入が済んだ請求書テンプレート


ここで、[差し込み文書]タブの[結果をプレビュー]をクリックしてオンにすると最初のデータがプレビューされる。

実際の差し込みでは、[差し込み文書]タブの[完了と差し込み]から[個々のドキュメントの編集(E)]でWordファイルを作成するか、[文書の印刷(P)]で印刷することになると思う。

実際に差し込みを行ったものが次図である。

f:id:nixeneko:20220110160329p:plain
差し込みを行った請求書


制限としては、この請求書の書式では注文内容の明細を示すための欄を16行しか確保していないので、16種類以上の品目が出てきた場合に望む結果が得るられないことになる。
とはいえそんなたくさんの品目を扱うならもっとちゃんとしたシステム導入するでしょという気もする。

入力を簡単にしたい

さて、差し込みは出来上がったものの、差し込み用のExcelデータを手入力するのはすごく面倒である。
そのため、必要最低限の情報を入力するExcelファイルを作り、適宜変換することにする。Excelは入力には便利なので。
もちろん注文内容をデータで持っているのであればそれを変換してやればそれで済むのだが、紙ベースで管理しているので入力するしかない。


変換は、本当であればすべてExcelでやりたかったのだが、やり方がよくわからなかったのでPythonで変換することとした。Pythonを選んだのは単に使い慣れてるからで、同じことができれば何でもよい。

f:id:nixeneko:20220110144725p:plain
請求書作成システム全体図

入力用Excelファイル → 変換 → 出力用Excelファイル → Word差し込み → 請求書Wordファイル ……という流れになる。

要件を考える

さて、Pythonで変換するにあたって、
出力にあたって必要なのは

  • 請求先名称
  • 請求年月日
  • 請求金額(合計金額)
  • 標準税率対象金額
  • 標準税額
  • 軽減税率対象金額
  • 軽減税額
  • 消費税計
  • 品目のリスト

であり、品目についてはさらに

  • 品名(軽減税率対象の場合は最後に「(※)」とつける)
  • 税込単価(税別でもいいが全体で統一する)
  • 個数
  • 小計

をもつ必要がある。

また、品目については、送料を最後にまとめて表示するようにしたい。


税率の計算では小数点以下を扱う必要が生じるが、これはdecimalを利用する。誤差があると困るのでfloatは使わない方がいい。


消費税の計算については、

  1. 標準税率対象金額から標準税額を計算し、整数に丸める
  2. 軽減税率対象金額から軽減税率を計算し、整数に丸める
  3. 標準税額と軽減税額を合計する

という順番(ただし1.と2.は順不同)で、税率ごとの対象金額の合計に対して整数に丸め、合計することになっている。
整数への丸め方(端数処理)は、切り捨て、切り上げ、四捨五入などのどれでもよいとのこと。とりあえず四捨五入にしておく…。
参考: 切り捨て?切り上げ?請求書の端数処理について解説 | 経営者から担当者にまで役立つバックオフィス基礎知識 | クラウド会計ソフト freee

入力用Excelファイルの構造を考える

入力用のExcelファイルは次のようなものを作った。

f:id:nixeneko:20220110145450p:plain
入力用Excelファイル

以下概略である。

  • 「請求書ID」が同一のものが1つの請求書となる。
  • 「被請求者」が請求先として記載される。
  • 「商品品目」はドロップダウンリストからの選択式とした。
    • ドロップダウンリストの定義は「品目リスト」シートのテーブルで行っている。
    • 自由入力も可能としている。
  • 「軽減」は軽減税率対象の場合はxを入力する。
    • 「商品品目」をドロップダウンリストから選択した場合は自動入力されるようにした。
  • 「単価」はドロップダウンリストから選択した場合は自動入力されるようにした。
  • 「送り先〒」に郵便番号を入力するか、「送り先住所」に都道府県を入力する。
    • 「送り先〒」は文字列で、入力すると隣の「送り先住所」に都道府県が入るようにしている。
      • 住所を都道府県から書かない人がいるので、これがないと調べる手間が増える。
    • 郵便番号から都道府県を引くのは「郵便番号表」 シートのテーブルで行っている。
  • 「サイズ」は荷物のサイズで、商品品目がリストにあれば自動入力される他、プルダウンで変更できる。
    • 大きな箱単位のものを想定しているので自動入力を用意しているが、発送する荷物の数と一対一対応しないことがほとんどの場合は自動入力をなくした方がよさそう。
  • 「送料単価」は「送り先住所」と「サイズ」から自動入力される。
    • 都道府県送料」シートから引いている。
  • 「送料個数」は「個数」と同一の数値が自動で入力されるようにしているが、任意で変更できる。
    • 必要がなければ空欄にするか0にする。
    • 商品の個数と一対一対応することがまれな場合は自動入力をなくした方がよさそう。
プルダウンリストの元の値にテーブルを利用する

プルダウンリストの[元の値]にテーブルを利用するには、INDIRECT()関数を使う。
連動するドロップダウンリストをテーブルを利用して作成する:Excelの基本操作

ドロップダウンリストと自由入力を混在させる

データの入力規則として、ドロップダウンリストと自由入力を混在させるためには、[データの入力規則]の[エラーメッセージ]タブの設定を変更する。
Excel:入力規則でドロップダウンリストと入力を混在させたい  −教えて!HELPDESK

郵便番号表の作成
  1. [データ]タブ → [データの取得] → [ファイルから(F)] → [テキストまたはCSVから(T)] → KEN_ALL.CSVを選択、[インポート]を押す。
  2. ウィンドウが開くので、[データ型検出]を[データ型を検出しない]に変更して[読み込み]を押す。
  3. 読み込まれるので、不要な列を消す。
  4. 全体を選択して、[セルの書式設定]→[表示形式]を[文字列]にして[OK]を押す。
  5. シート名を郵便番号表にする(しなくてもいい)。
  6. [テーブル デザイン]タブから[テーブル名]を郵便番号テーブルにする

変換スクリプトの用意

seikyusho_sample/convert.py at main · nixeneko/seikyusho_sample · GitHub
変換用のPythonスクリプトを用意した。

実行に必要なのは

である。

使い方

端末やコマンドプロンプト*2を開いて、

python convert.py 請求データ.xlsx --date 2022-01-06

などとすると、processed.xlsxというファイルが出力される。
コマンドラインオプションとして、--dateで日付指定、--min_id, --max_idでIDの範囲指定ができるようにしてある。


一度差し込み印刷のファイルを設定してしまえば、同じファイルパスをであれば請求書テンプレートのWordファイルを開くときに自動で読み込まれるので、請求書出力プロセスは次のようになる。

  1. 入力用Excelファイルへの入力
  2. pythonスクリプトで変換
  3. 請求書テンプレートのWordファイルを開き、差し込みを実行する

というわけで、かなり楽になった。

サンプルのダウンロード

次のリンクからダウンロードできます。
github.com
なお、動作保証等はしませんので、利用される場合は自己責任で行ってください。

さいごに

請求書を手書きからコンピュータ作成に移行しようというところだったので、ついでに適格請求書に対応できるような仕組みを準備した。

取扱商品が少なく固定されている場合は商品ごとに列を用意して数だけ入力するとかにしてもよいし、自分のところで扱っている商品によってシステムを構成することでかなり楽ができる。

効率化していきましょう。

*1:というか、レシートとか見ると税別で計算してる方式の方が多いような気もする。ここら辺正直良くわかってないので調べないといけない…。

*2:手元ではCygwin上のpython3を使って動作確認をしている。