Django Oscar (ECパッケージ) 日本利用ガイド

Python で(多分)最も使われている全部入り(フルスタック) Webフレームワーク、Django。そのDjango 上で動く、(多分)最も勢いがある ECパッケージ、Oscar。

その Oscar を日本で使うためのガイドとして書きました。

Django-Oscar ドキュメント: https://django-oscar.readthedocs.org/

リポジトリ: https://github.com/django-oscar/django-oscar

# Oscar の特徴

- Python + Django 環境上で動くので、経験者であればカスタマイズが容易
- Python3 でも動く
- Django は~~最新の~~ 1.8 必須。(追記:1.9リリースされてた)
- 修正BSDライセンス([BSD 3-Clause License (Revised)](https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29))。Djangoと同じ。
- Bootstrap 入り。デモテンプレートからレスポンシブになってます。
- 多言語対応済み。日本語のPOファイルも最初から入ってますが、それほど充実してはいないです。
- 多通貨対応済み。通貨の型は標準では小数点付き10進数なので、日本人の僕は最初少し戸惑った。
- 最初からさまざまな国の住所が登録できる。
- 決済システムは最初は入ってないので、pip で入れるか自分で書く。
- コアコード以外を柔軟にカスタマイズ可能。例えば、モデルにフィールドを追加したり、ビューの動作を変えたりなどは元コードに手を入れなくてもできる。(コアコードを修正するならモンキーパッチかリポジトリのフォークかな…)
- 商品(カタログ) は、クラス > プロダクト > 在庫&料金 という3段構成。
- 在庫&料金クラスは、通貨別にそれぞれ在庫が設定できる。
- カテゴリクラスがある。 カテゴリ:プロダクトは 多:多
- 商品の画像は、通常サイトから画像ファイルをアップロードする形式で、外部サービスに画像を登録して URL だけモデルに書く、という使い方は最初はできない。モデルのカスタマイズとテンプレート修正が必要。
- 税率は、税率クラスをハードコーディングで書く。
- スタイルシートは less
- 管理サイトにログインする管理者アカウントと、一般顧客のアカウントは同じモデル(テーブル)に作ることになる。パーミッションフラグで権限を分ける。このモデルの分離をしたいならかなり困難そう。

最初から入ってる機能

- レビュー機能
- ウィッシュリスト
- オファー (特定条件指定のセール機能)
- バウチャー(いわゆるクーポンコード)
- 管理サイト(ダッシュボード)
- サムネイル生成
- JS, CSS圧縮 (django-compressor)

など。

レビューなどの機能を OFF にするのは比較的簡単にできます。(settingsで可能)

そういえば、日本でよくある「ポイント」機能は入ってません。


# モデルのフォーク

Oscar 標準機能を延長(継承)したい場合、fork という機能を使います。

まず、プロジェクト直下に oscar_fork ( とか myapps とか ) といったディレクトリを作り、

```bash
$ ./manage.py oscar_fork_app catalogue oscar_fork
```

を実行すると、oscar_fork/catalogue が出来ます。

このフォークモジュールでは、

- モデルの拡張
- ビューの拡張、無効化
- URL の追加、無効化

などが行えます。

## モデルフィールドの追加例

例えば、catalogue.Product に、thumbnail_url フィールドを追加したいなら

```python:oscar_fork/catalogue/models.py
from django.db import models
from oscar.apps.catalogue.abstract_models import AbstractProduct

class Product(AbstractProduct):
    image_url = models.URLField(default='', blank=True)
    thumbnail_url = models.URLField(default='', blank=True)

from oscar.apps.catalogue.models import *  # noqa
```
として、INSTALLED_APPS に

```python:settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    ....
]
INSTALLED_APPS += get_core_apps([
    'oscar_fork.catalogue',
])
```

と書くことで、INSTALLED_APPS を延長します。

`./manage.py makemigrations` でマイグレーションファイルが出来るので、`./manage.py migrate` でスキーママイグレーションします。

## ビューの拡張

カート (checkout) のビューを拡張したいなら

```bash
$ ./manage.py oscar_fork_app checkout oscar_fork
```
してから

```python:oscar_fork/checkout.views.py
from oscar.apps.checkout import views

class IndexView(views.IndexView):
    def get(self, request, *args, **kwargs):
        # get処理...
```

のように書けます。

INSTALLED_APPS にも追加

```python:settings.py
INSTALLED_APPS += get_core_apps([
    'oscar_fork.catalogue',
    'oscar_fork.checkout',
])
```

## URLs の追加、編集

URLs を追加したり無効化したい場合は、app.py を書きます。

```bash
$ ./manage.py oscar_fork_app customer oscar_fork
```

```oscar_fork/customer/app.py
from oscar.apps.customer import app

class CustomerApplication(app.CustomerApplication):
    def get_urls(self):
        urls = [
            # Login, logout and register doesn't require login
            url(r'^login/$', self.login_view.as_view(), name='login'),
            url(r'^logout/$', self.logout_view.as_view(), name='logout'),
            # url(r'^register/$', self.register_view.as_view(),
            # name='register'),
```
register のURL をコメントアウトすることで、無効化したりできます。

INSTALLED_APPS にも追加。(詳細略)

# 日本国内向けに使うにはどうしたら良いか

## まずは、Djangoの settings を日本向けに。

```python:settings.py
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'
```

## Oscar 特有設定を日本向けに

```python:settings.py
OSCAR_DEFAULT_CURRENCY = 'JPY'

OSCAR_CURRENCY_FORMAT = '¤#,##0'
```

## 登録国を日本のみに

```bash
$ ./manage.py oscar_populate_countries
```

で、国モデル ( address_country ) に249カ国の初期データが入ります。

これの、is_shipping_country を Japan のみ 1に、他は 0 にすると、住所登録時に国名選択が出てこなくなります (日本がデフォルトになる)。

Django シェルからでも、SQL でも。

```mysql
mysql> UPDATE address_country SET is_shipping_country = 0 WHERE printable_name != 'Japan';
```

## POファイルをひたすら書いて翻訳

How do I translate Oscar? — django-oscar 0.5 documentation
https://django-oscar.readthedocs.org/en/releases-0.5/howto/how_do_i_translate_oscar.html

ここを参考に

```bash
$ mkdir locale i18n
$ ln -s $PATH_TO_OSCAR i18n/oscar
$ ./manage.py makemessages --symlinks --locale=ja
```

(うまくいかない場合、多分 gettext がインストールされてないとか。僕の場合(mac)は brew install gettext --force が必要でした)

locale/ja/LC_MESSAGES/django.po を編集

```
msgid "email address"
msgstr "メールアドレス"

msgid "First name"
msgstr "姓"

msgid "Last name"
msgstr "名"

msgid "View basket"
msgstr "カートを見る"

msgid "Basket total"
msgstr "合計金額"

msgid "Basket total:"
msgstr "合計金額:"

msgid "In stock"
msgstr "在庫有り"

msgid "Shipping"
msgstr "送料"

msgid "Total"
msgstr "小計"

msgid "Order total"
msgstr "合計金額"

msgid "State/County"
msgstr "都道府県"

msgid "City"
msgstr "市区町村"

msgid "First line of address"
msgstr "町名/字/番地"

msgid "Second line of address"
msgstr "建物/部屋番号"

...
```

(※ First name = 姓,  Last name = 名 は意図して書いており、間違ったわけではないです。こっちのほうが僕は使いやすいので。)

poファイルを書いたら、

```bash
$ ./manage.py compilemessages
```

を実行し mo ファイルを作っておく。これでサイトが日本語表示になります。

# 税率の変更

デフォルトは、固定税率 0% になってます。日本は消費税 8% なので変更が必要です。

税率は、partner/strategy.py にあるので

```bash
$ ./manage.py oscar_fork_app partner oscar_fork
```

してから、

```python:oscar_fork/partner/strategy.py
from decimal import Decimal as D
from oscar.apps.partner import strategy

class Selector(object):
    def strategy(self, request=None, user=None, **kwargs):
        return JPStrategy()

class JP(strategy.FixedRateTax):
    rate = D('0.08')
    exponent = D('1.')

    def pricing_policy(self, product, stockrecord):
        if not stockrecord:
            return prices.Unavailable()
        rate = self.get_rate(product, stockrecord)
        exponent = self.get_exponent(stockrecord)
        tax = (stockrecord.price_excl_tax * rate).quantize(
            exponent, rounding=ROUND_DOWN)
        return prices.TaxInclusiveFixedPrice(
            currency=stockrecord.price_currency,
            excl_tax=stockrecord.price_excl_tax,
            tax=tax)

    def parent_pricing_policy(self, product, children_stock):
        stockrecords = [x[1] for x in children_stock if x[1] is not None]
        if not stockrecords:
            return prices.Unavailable()

        # We take price from first record
        stockrecord = stockrecords[0]
        rate = self.get_rate(product, stockrecord)
        exponent = self.get_exponent(stockrecord)
        tax = (stockrecord.price_excl_tax * rate).quantize(
            exponent, rounding=ROUND_DOWN)

        return prices.FixedPrice(
            currency=stockrecord.price_currency,
            excl_tax=stockrecord.price_excl_tax,
            tax=tax)

class JPStrategy(strategy.UseFirstStockRecord, strategy.StockRequired,
                 JP, strategy.Structured):
    pass
```
このように書けます。

※ JPクラスが長ったらしくて少し冗長に見えると思います。
以前は、この JP クラスを

```python
class JP(strategy.FixedRateTax):
    rate = D('0.08')
````

こう書いていました。この場合、金額の小数点以下がそのまま扱われます。
例えば、税抜70円の商品の場合、税込75.6円として内部で計算されます。小数点以下を考慮します。

これだと、私のサービスだと不都合がありましたので、以下のように変更した所

```python
class JP(strategy.FixedRateTax):
    rate = D('0.08')
    exponent = D('1.')
````

小数点以下は四捨五入となります。税抜70円商品の場合、税込76円になります。

今回は、小数点以下は切り捨てとしたかったので、`pricing_policy`, `parent_pricing_policy` 両方のメソッドをオーバーライドし、`Decimal` の `quantize` メソッドの引数に `rounding=ROUND_DOWN` を入れるようにしました。

ただ 端数切捨てにしたかっただけでメソッドをまるごとオーバーライドするのはちょっとエレガントではないですね。なんか良い方法ないでしょうか。

`decimal.setcontext` で実現できるんでしょうかね。他に影響ありそうで怖くてできません。





例によって INSTALLED_APPS に追加

```python:settings.py
INSTALLED_APPS += get_core_apps([
    ...
    'oscar_fork.partner',
])
```

ロジックで分岐など書けるので、複雑な処理も書けます。関税など。

これで、ほぼ日本で使えるようになったと思います。

あとはひたすらテンプレート作成と翻訳ですね。


# トップページはどこ?

ちなみに。トップページは promotions の中にあります。
ビューは `oscar.apps.promotions.views.HomeView`、
テンプレートは `promotions/home.html` です。探すと少し時間がかかるので、書き記しておきます。

# その他必要な機能

## 決済モジュール

paypal は、 `django-oscar-paypal` モジュールがあって比較的簡単に使えるらしい。エクスプレスチェックアウト と payflow 対応。多分、エクスプレスチェックアウトはリンクが出るだけ。

僕は webpay でやろうと思ったので自分で書きました。

`payment.views.PaymentDetailsView` の `handle_payment` をオーバーライドして書くことになります。