aboutsummaryrefslogtreecommitdiffstats
path: root/servo
diff options
context:
space:
mode:
authorFilipp Lepalaan <filipp@mac.com>2015-08-04 10:11:24 +0300
committerFilipp Lepalaan <filipp@mac.com>2015-08-04 10:11:24 +0300
commit63b0fc6269b38edf7234b9f151b80d81f614c0a3 (patch)
tree555de3068f33f8dddb4619349bbea7d9b7c822fd /servo
downloadServo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.tar.gz
Servo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.tar.bz2
Servo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.zip
Initial commit
First public commit
Diffstat (limited to 'servo')
-rw-r--r--servo/__init__.py5
-rw-r--r--servo/api/__init__.py0
-rw-r--r--servo/api/serializers.py113
-rw-r--r--servo/celery.py22
-rw-r--r--servo/defaults.py75
-rw-r--r--servo/exceptions.py32
-rw-r--r--servo/fixtures/comptia.yaml410
-rw-r--r--servo/fixtures/initial_data.json10
-rw-r--r--servo/forms/__init__.py33
-rw-r--r--servo/forms/account.py104
-rw-r--r--servo/forms/admin.py557
-rw-r--r--servo/forms/base.py172
-rw-r--r--servo/forms/checkin.py303
-rw-r--r--servo/forms/customer.py113
-rw-r--r--servo/forms/devices.py94
-rw-r--r--servo/forms/invoices.py108
-rw-r--r--servo/forms/notes.py91
-rw-r--r--servo/forms/orders.py167
-rw-r--r--servo/forms/product.py228
-rw-r--r--servo/forms/repairs.py99
-rw-r--r--servo/forms/returns.py112
-rw-r--r--servo/lib/__init__.py0
-rw-r--r--servo/lib/middleware.py67
-rw-r--r--servo/lib/shorturl.py130
-rw-r--r--servo/lib/ucsv.py49
-rw-r--r--servo/locale/da_DK/LC_MESSAGES/django.mobin0 -> 378 bytes
-rw-r--r--servo/locale/da_DK/LC_MESSAGES/django.po5236
-rw-r--r--servo/locale/et_EE/LC_MESSAGES/django.mobin0 -> 33869 bytes
-rw-r--r--servo/locale/et_EE/LC_MESSAGES/django.po5460
-rw-r--r--servo/locale/fi_FI/LC_MESSAGES/django.mobin0 -> 51106 bytes
-rw-r--r--servo/locale/fi_FI/LC_MESSAGES/django.po5396
-rw-r--r--servo/locale/sv_SE/LC_MESSAGES/django.mobin0 -> 11934 bytes
-rw-r--r--servo/locale/sv_SE/LC_MESSAGES/django.po5299
-rw-r--r--servo/management/__init__.py0
-rw-r--r--servo/management/commands/__init__.py0
-rw-r--r--servo/management/commands/backup.py155
-rw-r--r--servo/management/commands/checkescalations.py71
-rw-r--r--servo/management/commands/checkmail.py58
-rw-r--r--servo/management/commands/cleandups.py42
-rw-r--r--servo/management/commands/cleanphones.py43
-rw-r--r--servo/management/commands/cleanup.py56
-rw-r--r--servo/management/commands/clearcache.py36
-rwxr-xr-xservo/management/commands/cron.py164
-rw-r--r--servo/management/commands/dbbackup.py43
-rw-r--r--servo/management/commands/dbdump.py37
-rw-r--r--servo/management/commands/dbrestore.py42
-rw-r--r--servo/management/commands/deleteorders.py54
-rw-r--r--servo/management/commands/fixfollowers.py43
-rw-r--r--servo/management/commands/fixproducts.py43
-rw-r--r--servo/management/commands/fixtimestamps.py46
-rw-r--r--servo/management/commands/importparts.py127
-rw-r--r--servo/management/commands/migratepayments.py44
-rw-r--r--servo/management/commands/migratestatuses.py54
-rw-r--r--servo/management/commands/migratetimezones.py41
-rw-r--r--servo/management/commands/obfuscate.py107
-rw-r--r--servo/management/commands/slugifycategories.py38
-rw-r--r--servo/management/commands/sutostaff.py43
-rw-r--r--servo/management/commands/tokenize.py39
-rw-r--r--servo/management/commands/updatecomponents.py44
-rw-r--r--servo/management/commands/updateorders.py47
-rw-r--r--servo/management/commands/updatepototals.py38
-rw-r--r--servo/management/commands/updateprices.py63
-rw-r--r--servo/management/commands/updaterepairs.py45
-rw-r--r--servo/messaging/__init__.py0
-rw-r--r--servo/messaging/sms.py193
-rw-r--r--servo/migrations/0001_initial.py1373
-rw-r--r--servo/migrations/0002_auto_20141123_2358.py21
-rw-r--r--servo/migrations/0003_auto_20141217_0029.py40
-rw-r--r--servo/migrations/0004_auto_20141229_0930.py20
-rw-r--r--servo/migrations/0004_auto_20141230_1630.py238
-rw-r--r--servo/migrations/0005_auto_20141229_2045.py21
-rw-r--r--servo/migrations/0006_merge.py15
-rw-r--r--servo/migrations/0007_auto_20150101_2318.py238
-rw-r--r--servo/migrations/0008_user_enable_notifications.py20
-rw-r--r--servo/migrations/0009_auto_20150101_2336.py25
-rw-r--r--servo/migrations/0010_auto_20150104_0018.py21
-rw-r--r--servo/migrations/0011_device_next_tether_policy.py20
-rw-r--r--servo/migrations/0012_auto_20150107_1711.py20
-rw-r--r--servo/migrations/0013_auto_20150204_1113.py25
-rw-r--r--servo/migrations/0014_orderstatus_duration.py20
-rw-r--r--servo/migrations/0015_auto_20150208_1629.py20
-rw-r--r--servo/migrations/0016_auto_20150316_1152.py70
-rw-r--r--servo/migrations/0017_auto_20150430_2233.py242
-rw-r--r--servo/migrations/0018_auto_20150505_0020.py20
-rw-r--r--servo/migrations/0019_auto_20150507_0048.py18
-rw-r--r--servo/migrations/0020_auto_20150514_0618.py24
-rw-r--r--servo/migrations/0021_auto_20150608_1703.py24
-rw-r--r--servo/migrations/0022_auto_20150612_0803.py24
-rw-r--r--servo/migrations/0023_auto_20150612_0822.py24
-rw-r--r--servo/migrations/__init__.py0
-rw-r--r--servo/models/__init__.py42
-rw-r--r--servo/models/account.py318
-rw-r--r--servo/models/calendar.py184
-rw-r--r--servo/models/common.py823
-rw-r--r--servo/models/customer.py314
-rw-r--r--servo/models/device.py523
-rw-r--r--servo/models/escalations.py125
-rw-r--r--servo/models/invoices.py226
-rw-r--r--servo/models/note.py617
-rw-r--r--servo/models/order.py1156
-rw-r--r--servo/models/parts.py407
-rw-r--r--servo/models/product.py654
-rw-r--r--servo/models/purchases.py353
-rw-r--r--servo/models/queue.py296
-rw-r--r--servo/models/repair.py641
-rw-r--r--servo/models/rules.py177
-rw-r--r--servo/models/shipments.py212
-rw-r--r--servo/stats/__init__.py0
-rw-r--r--servo/stats/forms.py109
-rw-r--r--servo/stats/queries.py247
-rw-r--r--servo/tasks.py133
-rwxr-xr-xservo/templates/about.html25
-rwxr-xr-xservo/templates/accounts/calendar_form.html12
-rwxr-xr-xservo/templates/accounts/calendars.html55
-rwxr-xr-xservo/templates/accounts/delete_calendar.html17
-rwxr-xr-xservo/templates/accounts/delete_calendar_event.html13
-rwxr-xr-xservo/templates/accounts/edit_calendar_event.html14
-rwxr-xr-xservo/templates/accounts/login.html22
-rwxr-xr-xservo/templates/accounts/logout.html17
-rwxr-xr-xservo/templates/accounts/orders.html59
-rwxr-xr-xservo/templates/accounts/print_calendar.html82
-rwxr-xr-xservo/templates/accounts/register.html14
-rwxr-xr-xservo/templates/accounts/settings.html46
-rwxr-xr-xservo/templates/accounts/stats.html42
-rwxr-xr-xservo/templates/accounts/tabs.html22
-rw-r--r--servo/templates/accounts/updates.html65
-rwxr-xr-xservo/templates/accounts/view_calendar.html66
-rw-r--r--servo/templates/admin/backups.html17
-rw-r--r--servo/templates/admin/checklist/form.html31
-rw-r--r--servo/templates/admin/checklist/index.html6
-rw-r--r--servo/templates/admin/fields/form.html17
-rw-r--r--servo/templates/admin/fields/index.html30
-rw-r--r--servo/templates/admin/fields/remove.html8
-rwxr-xr-xservo/templates/admin/gsx/form.html20
-rwxr-xr-xservo/templates/admin/gsx/index.html6
-rwxr-xr-xservo/templates/admin/gsx/remove.html13
-rw-r--r--servo/templates/admin/index.html50
-rw-r--r--servo/templates/admin/inventory/index.html18
-rw-r--r--servo/templates/admin/locations/form.html40
-rw-r--r--servo/templates/admin/locations/index.html6
-rw-r--r--servo/templates/admin/notifications/form.html5
-rw-r--r--servo/templates/admin/notifications/index.html39
-rw-r--r--servo/templates/admin/queues/form.html81
-rw-r--r--servo/templates/admin/queues/index.html6
-rw-r--r--servo/templates/admin/queues/remove.html17
-rw-r--r--servo/templates/admin/settings.html136
-rw-r--r--servo/templates/admin/sites/edit_site.html9
-rw-r--r--servo/templates/admin/sites/index.html23
-rw-r--r--servo/templates/admin/statuses/form.html21
-rw-r--r--servo/templates/admin/statuses/index.html6
-rw-r--r--servo/templates/admin/statuses/remove.html17
-rw-r--r--servo/templates/admin/tags/form.html17
-rw-r--r--servo/templates/admin/tags/index.html37
-rw-r--r--servo/templates/admin/templates/form.html21
-rw-r--r--servo/templates/admin/templates/list_templates.html7
-rw-r--r--servo/templates/admin/users/delete_group.html11
-rw-r--r--servo/templates/admin/users/form.html70
-rw-r--r--servo/templates/admin/users/group_form.html23
-rw-r--r--servo/templates/admin/users/groups.html37
-rw-r--r--servo/templates/admin/users/index.html51
-rw-r--r--servo/templates/admin/users/remove.html17
-rw-r--r--servo/templates/admin/users/tabs.html7
-rw-r--r--servo/templates/admin/users/upload_users.html13
-rw-r--r--servo/templates/checkin/confirmation.html13
-rw-r--r--servo/templates/checkin/customer_form.html87
-rw-r--r--servo/templates/checkin/device_form.html62
-rw-r--r--servo/templates/checkin/error.html15
-rw-r--r--servo/templates/checkin/index.html95
-rw-r--r--servo/templates/checkin/newindex.html146
-rw-r--r--servo/templates/checkin/status-show.html35
-rw-r--r--servo/templates/checkin/status.html21
-rw-r--r--servo/templates/checkin/terms.html9
-rw-r--r--servo/templates/checkin/thanks.html26
-rwxr-xr-xservo/templates/customers/choose-list.html8
-rwxr-xr-xservo/templates/customers/choose.html45
-rwxr-xr-xservo/templates/customers/delete_group.html17
-rwxr-xr-xservo/templates/customers/edit_group.html8
-rw-r--r--servo/templates/customers/find.html95
-rwxr-xr-xservo/templates/customers/form.html51
-rwxr-xr-xservo/templates/customers/index.html84
-rwxr-xr-xservo/templates/customers/list.html11
-rw-r--r--servo/templates/customers/merge.html19
-rwxr-xr-xservo/templates/customers/move.html20
-rwxr-xr-xservo/templates/customers/remove.html17
-rw-r--r--servo/templates/customers/results-merge.html5
-rwxr-xr-xservo/templates/customers/search-results.html5
-rwxr-xr-xservo/templates/customers/search.html44
-rw-r--r--servo/templates/customers/upload.html13
-rwxr-xr-xservo/templates/customers/view.html119
-rwxr-xr-xservo/templates/default.html185
-rwxr-xr-xservo/templates/default_print.html23
-rwxr-xr-xservo/templates/devices/accessories_edit.html40
-rwxr-xr-xservo/templates/devices/choose-error.html5
-rwxr-xr-xservo/templates/devices/choose-list.html12
-rwxr-xr-xservo/templates/devices/choose.html26
-rw-r--r--servo/templates/devices/diagnostic_error.html1
-rw-r--r--servo/templates/devices/diagnostic_init.html16
-rw-r--r--servo/templates/devices/diagnostic_ios.html20
-rwxr-xr-xservo/templates/devices/diagnostic_results.html22
-rwxr-xr-xservo/templates/devices/diagnostics.html13
-rwxr-xr-xservo/templates/devices/find.html66
-rwxr-xr-xservo/templates/devices/form.html44
-rwxr-xr-xservo/templates/devices/get_info.html45
-rwxr-xr-xservo/templates/devices/index.html78
-rwxr-xr-xservo/templates/devices/list.html38
-rwxr-xr-xservo/templates/devices/parts.html42
-rwxr-xr-xservo/templates/devices/remove.html17
-rwxr-xr-xservo/templates/devices/search.html15
-rwxr-xr-xservo/templates/devices/search_gsx.html41
-rwxr-xr-xservo/templates/devices/search_gsx_error.html19
-rwxr-xr-xservo/templates/devices/search_gsx_parts.html30
-rwxr-xr-xservo/templates/devices/search_gsx_repairs.html27
-rwxr-xr-xservo/templates/devices/search_gsx_results.html5
-rwxr-xr-xservo/templates/devices/search_gsx_warranty.html71
-rwxr-xr-xservo/templates/devices/specs.html42
-rwxr-xr-xservo/templates/devices/summary.html48
-rwxr-xr-xservo/templates/devices/upload_devices.html13
-rwxr-xr-xservo/templates/devices/view.html83
-rwxr-xr-xservo/templates/dropdown_snippet.html8
-rw-r--r--servo/templates/error.html49
-rwxr-xr-xservo/templates/form_buttons.html5
-rwxr-xr-xservo/templates/form_field_snippet.html30
-rwxr-xr-xservo/templates/form_snippet.html7
-rw-r--r--servo/templates/generic/admin_list.html18
-rwxr-xr-xservo/templates/generic/button_dropdown.html12
-rwxr-xr-xservo/templates/generic/delete.html17
-rwxr-xr-xservo/templates/generic/index.html23
-rwxr-xr-xservo/templates/invoices/index.html106
-rwxr-xr-xservo/templates/invoices/view_invoice.html0
-rwxr-xr-xservo/templates/login.html26
-rwxr-xr-xservo/templates/modal.html19
-rwxr-xr-xservo/templates/notes/edit_escalation.html17
-rwxr-xr-xservo/templates/notes/find.html39
-rwxr-xr-xservo/templates/notes/form.html112
-rwxr-xr-xservo/templates/notes/list_notes.html75
-rw-r--r--servo/templates/notes/messages.html22
-rwxr-xr-xservo/templates/notes/remove.html17
-rwxr-xr-xservo/templates/notes/search-results.html25
-rwxr-xr-xservo/templates/notes/search.html18
-rwxr-xr-xservo/templates/notes/templates.html10
-rwxr-xr-xservo/templates/notes/view_escalation.html1
-rwxr-xr-xservo/templates/notes/view_note.html38
-rwxr-xr-xservo/templates/oauth/challenge.html0
-rw-r--r--servo/templates/orders/batch_process.html14
-rwxr-xr-xservo/templates/orders/checklists.html10
-rwxr-xr-xservo/templates/orders/close.html17
-rwxr-xr-xservo/templates/orders/close_repair.html17
-rwxr-xr-xservo/templates/orders/customer.html52
-rwxr-xr-xservo/templates/orders/delete_order.html17
-rwxr-xr-xservo/templates/orders/devices.html139
-rwxr-xr-xservo/templates/orders/dispatch.html115
-rwxr-xr-xservo/templates/orders/edit.html115
-rwxr-xr-xservo/templates/orders/edit_product.html33
-rwxr-xr-xservo/templates/orders/events.html21
-rwxr-xr-xservo/templates/orders/followers.html28
-rwxr-xr-xservo/templates/orders/gsx_repair_form.html94
-rw-r--r--servo/templates/orders/history.html9
-rwxr-xr-xservo/templates/orders/index.html55
-rwxr-xr-xservo/templates/orders/list.html43
-rwxr-xr-xservo/templates/orders/list_products.html22
-rwxr-xr-xservo/templates/orders/notes.html84
-rwxr-xr-xservo/templates/orders/parts.html36
-rwxr-xr-xservo/templates/orders/print_confirmation.html193
-rwxr-xr-xservo/templates/orders/print_dispatch.html38
-rwxr-xr-xservo/templates/orders/print_quote.html3
-rwxr-xr-xservo/templates/orders/print_receipt.html46
-rwxr-xr-xservo/templates/orders/products.html98
-rwxr-xr-xservo/templates/orders/remove_customer.html17
-rwxr-xr-xservo/templates/orders/remove_device.html17
-rwxr-xr-xservo/templates/orders/remove_product.html17
-rwxr-xr-xservo/templates/orders/repair.html63
-rwxr-xr-xservo/templates/orders/reserve_products.html13
-rwxr-xr-xservo/templates/orders/search.html7
-rwxr-xr-xservo/templates/orders/statuses.html7
-rwxr-xr-xservo/templates/orders/tabs.html8
-rwxr-xr-xservo/templates/orders/toggle_flagged.html1
-rwxr-xr-xservo/templates/orders/toggle_follow.html1
-rwxr-xr-xservo/templates/orders/toolbar.html116
-rwxr-xr-xservo/templates/pagination.html18
-rwxr-xr-xservo/templates/products/category_form.html13
-rwxr-xr-xservo/templates/products/choose-list.html9
-rwxr-xr-xservo/templates/products/choose.html25
-rwxr-xr-xservo/templates/products/delete_category.html17
-rwxr-xr-xservo/templates/products/form.html90
-rwxr-xr-xservo/templates/products/get_info.html59
-rwxr-xr-xservo/templates/products/index.html104
-rwxr-xr-xservo/templates/products/index_outgoing.html56
-rwxr-xr-xservo/templates/products/list.html27
-rwxr-xr-xservo/templates/products/list_rows.html55
-rwxr-xr-xservo/templates/products/receive_item.html51
-rwxr-xr-xservo/templates/products/remove.html13
-rwxr-xr-xservo/templates/products/search.html20
-rwxr-xr-xservo/templates/products/tabs.html21
-rwxr-xr-xservo/templates/products/upload_gsx_parts.html13
-rwxr-xr-xservo/templates/products/upload_products.html13
-rwxr-xr-xservo/templates/products/view.html205
-rwxr-xr-xservo/templates/products/view_incoming.html1
-rwxr-xr-xservo/templates/purchases/edit_po.html92
-rwxr-xr-xservo/templates/purchases/list_pos.html132
-rwxr-xr-xservo/templates/purchases/order_stock.html17
-rwxr-xr-xservo/templates/purchases/view_po.html56
-rwxr-xr-xservo/templates/repairs/add_part.html22
-rwxr-xr-xservo/templates/repairs/check_parts.html10
-rwxr-xr-xservo/templates/repairs/delete_part.html12
-rwxr-xr-xservo/templates/repairs/delete_repair.html16
-rwxr-xr-xservo/templates/repairs/get_details.html27
-rwxr-xr-xservo/templates/repairs/part_menu.html33
-rw-r--r--servo/templates/rules/form.html142
-rw-r--r--servo/templates/rules/list.html6
-rw-r--r--servo/templates/rules/list_rules.html22
-rwxr-xr-xservo/templates/search/results/articles.html10
-rwxr-xr-xservo/templates/search/results/customers.html26
-rwxr-xr-xservo/templates/search/results/devices.html10
-rwxr-xr-xservo/templates/search/results/gsx.html41
-rwxr-xr-xservo/templates/search/results/gsx_error.html4
-rwxr-xr-xservo/templates/search/results/gsx_notfound.html4
-rwxr-xr-xservo/templates/search/results/gsx_repair_details.html19
-rwxr-xr-xservo/templates/search/results/gsx_results.html5
-rwxr-xr-xservo/templates/search/results/gsx_warranty.html49
-rwxr-xr-xservo/templates/search/results/notes.html20
-rwxr-xr-xservo/templates/search/results/orders.html10
-rwxr-xr-xservo/templates/search/results/products.html10
-rwxr-xr-xservo/templates/search/spotlight.html37
-rwxr-xr-xservo/templates/shipments/add_to_return-results.html8
-rwxr-xr-xservo/templates/shipments/add_to_return.html14
-rwxr-xr-xservo/templates/shipments/edit_bulk_return.html101
-rwxr-xr-xservo/templates/shipments/index.html46
-rwxr-xr-xservo/templates/shipments/list_bulk_returns.html37
-rwxr-xr-xservo/templates/shipments/list_incoming.html61
-rwxr-xr-xservo/templates/shipments/list_returns.html44
-rwxr-xr-xservo/templates/shipments/submit_bulk_return.html15
-rwxr-xr-xservo/templates/shipments/update_part.html14
-rwxr-xr-xservo/templates/shipments/view_bulk_return.html35
-rw-r--r--servo/templates/snippets/alert.html1
-rwxr-xr-xservo/templates/snippets/control_group.html3
-rwxr-xr-xservo/templates/snippets/dropdown.html11
-rwxr-xr-xservo/templates/snippets/dropdown_menu.html5
-rwxr-xr-xservo/templates/snippets/error_modal.html10
-rwxr-xr-xservo/templates/snippets/filtering_form.html15
-rwxr-xr-xservo/templates/snippets/form_field.html12
-rwxr-xr-xservo/templates/snippets/form_field_label.html1
-rwxr-xr-xservo/templates/snippets/form_input.html22
-rwxr-xr-xservo/templates/snippets/modal.html14
-rwxr-xr-xservo/templates/stats/index.html68
-rwxr-xr-xservo/templates/stats/locations.html33
-rw-r--r--servo/templates/stats/newstats.html87
-rwxr-xr-xservo/templates/stats/plot_snippet.html9
-rwxr-xr-xservo/templates/stats/queues.html29
-rwxr-xr-xservo/templates/stats/sales.html33
-rwxr-xr-xservo/templates/stats/statuses.html21
-rwxr-xr-xservo/templates/tabbed_form.html14
-rwxr-xr-xservo/templates/three_column_layout.html27
-rwxr-xr-xservo/templates/two_column_layout.html18
-rw-r--r--servo/templatetags/__init__.py0
-rw-r--r--servo/templatetags/servo_tags.py189
-rw-r--r--servo/tests/__init__.py0
-rw-r--r--servo/tests/create_order.json19
-rw-r--r--servo/tests/test_functional.py135
-rwxr-xr-xservo/tests/test_unit.py66
-rw-r--r--servo/urls/__init__.py0
-rw-r--r--servo/urls/account.py45
-rw-r--r--servo/urls/admin.py76
-rw-r--r--servo/urls/api.py34
-rw-r--r--servo/urls/checkin.py15
-rw-r--r--servo/urls/customer.py29
-rw-r--r--servo/urls/default.py73
-rw-r--r--servo/urls/device.py52
-rw-r--r--servo/urls/invoices.py10
-rw-r--r--servo/urls/note.py29
-rw-r--r--servo/urls/order.py105
-rw-r--r--servo/urls/products.py56
-rw-r--r--servo/urls/purchases.py25
-rw-r--r--servo/urls/repairs.py11
-rw-r--r--servo/urls/rules.py10
-rw-r--r--servo/urls/sales.py10
-rw-r--r--servo/urls/search.py27
-rw-r--r--servo/urls/shipments.py28
-rw-r--r--servo/urls/stats.py13
-rw-r--r--servo/validators.py54
-rw-r--r--servo/views/__init__.py0
-rw-r--r--servo/views/account.py450
-rw-r--r--servo/views/admin.py778
-rw-r--r--servo/views/api.py401
-rw-r--r--servo/views/checkin.py418
-rw-r--r--servo/views/customer.py505
-rw-r--r--servo/views/device.py605
-rw-r--r--servo/views/error.py53
-rw-r--r--servo/views/events.py44
-rw-r--r--servo/views/files.py52
-rw-r--r--servo/views/gsx.py349
-rw-r--r--servo/views/invoices.py199
-rw-r--r--servo/views/note.py435
-rw-r--r--servo/views/order.py990
-rw-r--r--servo/views/product.py474
-rw-r--r--servo/views/purchases.py242
-rw-r--r--servo/views/queue.py40
-rw-r--r--servo/views/rules.py101
-rw-r--r--servo/views/search.py254
-rw-r--r--servo/views/shipments.py392
-rw-r--r--servo/views/stats.py443
-rw-r--r--servo/views/tags.py37
401 files changed, 53225 insertions, 0 deletions
diff --git a/servo/__init__.py b/servo/__init__.py
new file mode 100644
index 0000000..b64e43e
--- /dev/null
+++ b/servo/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import absolute_import
+
+# This will make sure the app is always imported when
+# Django starts so that shared_task will use this app.
+from .celery import app as celery_app
diff --git a/servo/api/__init__.py b/servo/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/api/__init__.py
diff --git a/servo/api/serializers.py b/servo/api/serializers.py
new file mode 100644
index 0000000..7a355df
--- /dev/null
+++ b/servo/api/serializers.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from rest_framework import serializers
+
+from servo.models import (User, Device, Order, Note,
+ ServiceOrderItem, Customer,)
+
+
+class CustomerSerializer(serializers.HyperlinkedModelSerializer):
+ devices = serializers.HyperlinkedRelatedField(
+ many=True,
+ read_only=True,
+ view_name='api-device_detail'
+ )
+ orders = serializers.HyperlinkedRelatedField(
+ many=True,
+ read_only=True,
+ view_name='api-order_detail'
+ )
+
+ class Meta:
+ model = Customer
+ fields = ('name', 'fullname', 'phone', 'email',
+ 'street_address', 'zip_code', 'city',
+ 'country', 'devices', 'orders',)
+
+class DeviceSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Device
+ fields = (
+ 'sn',
+ 'imei',
+ 'unlocked',
+ 'purchased_on',
+ 'purchase_country',
+ 'description',
+ 'warranty_status',
+ 'contract_end_date',
+ 'image_url',
+ 'fmip_active',
+ 'parts_and_labor_covered',
+ 'configuration',
+ 'applied_activation_policy',
+ 'next_tether_policy',
+ )
+
+
+class UserSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = User
+ fields = ('id', 'username', 'full_name', 'email',)
+
+
+class NoteSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Note
+ fields = ('id', 'subject', 'body', 'code', 'sender',
+ 'recipient', 'created_at', 'labels',)
+
+
+class OrderStatusSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Order
+ fields = ('status_name',)
+
+
+class OrderSerializer(serializers.HyperlinkedModelSerializer):
+ user = serializers.HyperlinkedRelatedField(
+ read_only=True,
+ view_name='api-user_detail'
+ )
+ notes = serializers.HyperlinkedRelatedField(
+ many=True,
+ read_only=True,
+ view_name='api-note_detail'
+ )
+ products = serializers.HyperlinkedRelatedField(
+ many=True,
+ read_only=True,
+ view_name='api-order_items'
+ )
+ class Meta:
+ model = Order
+ fields = ('created_at', 'status', 'notes', 'products', 'user',)
+
+
+class ServiceOrderItemSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = ServiceOrderItem
diff --git a/servo/celery.py b/servo/celery.py
new file mode 100644
index 0000000..8c87814
--- /dev/null
+++ b/servo/celery.py
@@ -0,0 +1,22 @@
+from __future__ import absolute_import
+
+import os
+
+from celery import Celery
+
+# set the default Django settings module for the 'celery' program.
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings')
+
+from django.conf import settings
+
+app = Celery('servo')
+
+# Using a string here means the worker will not have to
+# pickle the object when using Windows.
+app.config_from_object('django.conf:settings')
+app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
+
+
+@app.task(bind=True)
+def debug_task(self):
+ print('Request: {0!r}'.format(self.request))
diff --git a/servo/defaults.py b/servo/defaults.py
new file mode 100644
index 0000000..1eeedf9
--- /dev/null
+++ b/servo/defaults.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+"""
+defaults.py
+Contains callables for model default values
+"""
+import uuid
+import settings
+import local_settings
+from decimal import Decimal
+
+from django.db import connection
+from django.core.cache import cache
+
+def _get(key):
+ cached = cache.get('defaults', {})
+
+ if not cached.get(key):
+ cursor = connection.cursor()
+ cursor.execute("SELECT key, value FROM servo_configuration")
+ for r in cursor.fetchall():
+ cached[r[0]] = r[1]
+
+ cache.set('defaults', cached)
+
+ return cached.get(key)
+
+def country():
+ return local_settings.INSTALL_COUNTRY
+
+def site_id():
+ return settings.SITE_ID
+
+def locale():
+ return settings.INSTALL_LOCALE
+
+def uid():
+ return str(uuid.uuid1()).upper()
+
+def subject():
+ return _get('default_subject')
+
+def vat():
+ val = _get('pct_vat') or 0.0
+ return Decimal(val)
+
+def margin(sum=0.0):
+ return _get('pct_margin') or 0.0
+
+def gsx_account():
+ return _get('gsx_account')
diff --git a/servo/exceptions.py b/servo/exceptions.py
new file mode 100644
index 0000000..5ecddb7
--- /dev/null
+++ b/servo/exceptions.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+class ServoException(Exception):
+ """docstring for ServoException"""
+ def __init__(self, arg):
+ super(ServoException, self).__init__()
+ self.arg = arg
+ \ No newline at end of file
diff --git a/servo/fixtures/comptia.yaml b/servo/fixtures/comptia.yaml
new file mode 100644
index 0000000..e3a48fa
--- /dev/null
+++ b/servo/fixtures/comptia.yaml
@@ -0,0 +1,410 @@
+"0":
+ category: General
+ symptoms:
+ 000: Not Applicable
+
+"E":
+ category: iPod
+ symptoms:
+ E01: Repairable accidental damage (OOW) - Customer Satisfaction codes
+ E02: Controls not responding (wheel-button-touch screen)
+ E03: Any Battery issue
+ E04: Any Display issue
+ E05: Input Output issue (audio-video-USB-WiFi)
+ E06: Alert messages appearing on screen or computer
+ E07: Will not boot-dead-no power-unusually hot
+ E08: Fails Service diagnostics
+ E09: Issue with accessories
+ Z46: REP iPod Swollen battery
+
+"B":
+ category: iPhone
+ symptoms:
+ B08: No Service - dropped calls # No service or data connection at all. Do not use for dropped calls or activation issues.
+ B09: Issue with Accessory # Issue With Accessory
+ B0A: Rear/iSight Camera Issue # Problems with the iPhone built in Rear/iSight camera, taking photos.
+ B0J: iTunes Connection Issue # Not seen by computer, content transfer via USB cable. Not able to connect/restore via iTunes. Issue is caused by accessory use B09.
+ B0M: Unusual Noise or Temperature # Temperature or noise related issues. Unusual temperature, unusually noise device.
+ B0N: Front / FaceTime Camera # Problems with the iPhone built in Front/FaceTime camera, taking photos.
+ B26: Frozen - At Apple Logo # Frozen at Apple Logo, system wide freezing.
+ B32: Vibration Motor # No vibration felt when selecting settings> sounds>vibrate. For Ringer switch issues use B37.
+ B33: SIM # SIM card not recognized, SIM card issues. Do not use for activation issues.
+ B35: Touch ID # Cannot enroll, not reading fingerprint, not unlocking phone. For Home button issues, use B36.
+ B36: Button/Switch - Home # Tactility or mechanical issues
+ B37: Button/Switch - Ringer/Side switch # Tactility or mechanical issues
+ B38: Button/Switch - Sleep/Wake # Tactility or mechanical issues
+ B39: Button/Switch - Volume # Tactility or mechanical issues
+ B45: WiFi/Bluetooth # WiFi/Bluetooth related issues
+ B54: Display - Image Quality # Bad colors, out of focus, white screen, ambient light sensor issues, lines/blocks.
+ B56: Display - Multi Touch # All or specific area not recognizing touch, too sensitive to touch
+ B57: Display - Pixels/Foreign Material # Stuck, missing, or dead pixels. Or foreign material in the display.
+ B5B: Sensors - Proximity Sensor Issue # Proximity Sensor does not function properly
+ B5C: Sensors - Accelerometer/Gyroscope # Issues related to compass and other location based symptoms such as GPS
+ B5H: Display - Blank/Black Screen # Device has power, is recognized by iTunes, but display remains black.
+ B5J: Any Apple Pay issues related to the iPhone hardware
+ B63: Sound - Internal Speaker # Sound issues related to the built-in speaker
+ B64: Sound - Internal Microphone # Issues related to built-in microphones
+ B68: Sound - Headset Jack # Sound related issues related to the headset/ headphone jack on the device
+ B7A: Receiver / Calls - Audio Quality # Audio issues related to Receiver or on phone calls. Poor or low audio, distortion, etc.
+ B7B: Dropped Calls / Signal Strength # All cellular connection related issues where a signal is present.
+ B81: Power - Unexpected Power Off # Device unexpectedly powers off or restarts at random. Do not replace if related to an app or any software.
+ B83: Power - Will Not Power On # No power at all, not recognized by iTunes, not a blank/black display issue, shows no signs of power.
+ B8A: Power - Any Battery Issue # Any Battery related issue including battery life too short, battery won’t charge, etc.
+ B90: Physical Damage - Liquid Damage # Liquid damage related issues
+ B91: Physical Damage - Other # Damage to phone modules (for example, camera lens, headphone jack, dock connector). Not for display or enclosure damage.
+ B92: Physical Damage - Multiple Cracks # Multiple cracks on display
+ B95: Physical Damage - Enclosure # Any enclosure damage related issues. Chips, scratches, dings, cracks in the housing/enclosure. Includes clicking or noise caused by physical damage.
+ B98: Physical Damage - Glass Single Crack # Single hairline crack on display with no point of impact.
+ Z47: REP iPhone Swollen battery
+ Z52: REP iPhone5 Sleep/Wake Button
+ Z53: REP Sleep/Wake Button (Multi Issue)
+ Z54: REP Sleep/Wake Button (Distributor)
+ Z55: REP iPhone 5 SWB Loaner
+ Z56: REP iPhone 5 Loaner Return No Power
+ Z57: REP European 5W USB power adapter
+ Z58: REP European iPhone 5 Battery Replacement
+
+"F":
+ category: iPad
+ symptoms:
+ F0A: LCI - Liquid Contamination Indicator On
+ F0B: Oxidation/Corrosion - No LCI Activated
+ F1A: Physical Damage - Enclosure
+ F1B: Physical Damage - LCD/Display
+ F1C: Engraving Issue
+ F1D: Cosmetic - Enclosure
+ F1E: Cosmetic - Display
+ F2B: Touch/Multi-Touch Control Issues
+ F2C: Sensor - Accelerometer, ALS, Gyro, Hall Issue
+ F2D: Home Button Issue
+ F2E: Sleep/Wake/Power Button Issue
+ F2F: Volume/Vibrate Button Issue
+ F2G: Touch ID Functionality
+ F3B: Charging Issue
+ F3C: Failed Battery Diagnostic
+ F3D: Battery Run Time Issue
+ F4B: Light Leakage Issue
+ F4C: Vertical, Horizontal lines or band on display
+ F4D: Video Noise/Flickering Issue
+ F4E: Blank Screen
+ F4F: Display - Other Functional Issue
+ F5A: Connectivity - USB
+ F5B: Connectivity - Cellular Data Issue
+ F5J: Audio Sound - Headset Jack
+ F5K: Audio Sound - Internal Speaker
+ F5L: Microphone Issue
+ F5M: Front Camera Issue
+ F5N: Rear Camera Issue
+ F5P: Connectivity - Wi-Fi 2.4GHz Issue
+ F5Q: Connectivity - Wi-Fi 5GHz Issue
+ F5H: Connectivity - Bluetooth
+ F6A: Alert Messages On Screen or Computer
+ F7C: Won't Boot - Has Power
+ F7D: No Power/Dead Issue
+ F7E: Unexpected Power Off/Reboot
+ F7B: Unusually hot
+ F7F: Can't Restore/Activate
+ F8A: Fails Functional Test
+ F9B: Issue with Power Adapter
+ F9C: Issue with USB Charging Cable
+ F9D: Issue with Other Accessories
+ Z45: REP iPad Swollen Battery
+ F9Z: iPad Enterprise Entitlement Replacement
+
+"5":
+ category: Logic Boards (MLB/MPU/Video Boards)
+ symptoms:
+ M01: No Power/No Light
+ M02: Has Power/Light But Will Not Boot
+ M03: Caused No Video
+ M04: Caused Video Distortion
+ M05: Hang/Freeze Up
+ M06: Kernel Panic-Restart Required Message
+ M07: Memory Errors/Not Recognized
+ M08: Random Shutdown w/Reset During Use
+ M09: Caused Audio Issue
+ M10: Ethernet Port/Device Issue
+ M11: Airport/Bluetooth
+ M12: Firewire Port/Device Issues
+ M13: Caused Camera Issue
+ M14: Modem Issues
+ M15: USB Port/Device Issues (Not Power)
+ M16: Caused Keyboard/Trackpad Issue
+ M17: I/O Expansion Slot
+ M18: Caused Fan/Thermal Issue
+ M19: Cannot Detect Hard/Optical Drive
+ M20: Does Not Detect/Charge Battery
+ M21: Won't Detect Working Power Adapter
+ M22: Sleep/Wake Issue
+ M23: Sensors Test Failed
+ M24: Connectors - Broken/Damaged
+ M25: No Backlight/Has Boot Image
+ M26: No Video to External Video
+ M27: SD Card Issue
+ M28: HDMI Issue
+ M29: Caused Whole Screen Flicker
+ M30: No Power/No Adapter LED
+ M31: Video Distortion On External Display
+ M32: Thunderbolt Display Functionality Issue
+ M33: Thunderbolt Port Inoperative
+ M34: Thunderbolt not passing enough power
+ M35: Airport Not Recognized
+ M36: Bluetooth Not Recognized
+ M37: USB Device Not Detected
+ M38: USB Port Has Insufficient Power
+ M39: Boots To 3 Beeps-Memory not recog
+ M85: Unusually Hot - Overheat
+ M90: Liquid Spill/LSI Tripped
+ M99: Un-Categorized Symptom
+ Z03: MB Pro NVIDIA Issue
+ Z04: MB Pro NVIDIA Issue (Multiple Issues)
+ Z17: ATI 2600 XT-No Video
+ Z18: ATI 2600 XT-No Video/multi-issue
+ Z19: ATI 2600 XT-Distorted Video
+ Z20: ATI 2600 XT-Distorted Video/multi-issue
+ Z33: MBPRO 15 Kernel Panic
+ Z34: MBPRO 15 Kernel Panic (multi-issue)
+ Z48: REP iMac 2011 Video Card (Radeon 6970M)
+ Z49: REP iMac 2011 Video (multi-issue depot)
+ Z62: REP MBP 2011 Graphics
+ Z63: REP MBP 2011 Graphics (Multi Issue)
+ Z64: REP MBPr 2012/2013 MLB
+ Z65: REP MBPr 2012/2013 MLB (Multi Issue)
+
+"3":
+ category: Mass Storage (HDD/ODD and SSD)
+ symptoms:
+ J01: Optical Drive Won't Accept Optical Media
+ J02: Optical Drive Won't Eject Optical Media
+ J03: Optical Media Read/Write Data Error
+ J04: Optical Drive Noisy
+ J05: Optical Drive Physical Damage
+ J06: Optical Video Problems
+ J07: Optical Drive Not Performing To Spec
+ J08: Firmware Issues
+ J09: Optical Drive Not Recognized
+ H01: Hard Drive Not Recognized/Mount
+ H02: Hard Drive Can't Boot
+ H03: Hard Drive Read/Write Problem
+ H04: Drive - Pins/Connector bent/broken
+ H05: Hard Drive Bad Sector/Defective
+ H06: Hard Drive Operational But Noisy
+ H07: Hard Drive Formatting Issues
+ H08: Hard Drive Firmware Issues
+ H90: Liquid Spill/LSI Tripped
+ H99: Un-Categorized Symptom
+ Z29: iMac (Mid 2011) Hard Drive Program
+ Z30: iMac (Mid 2011) Hard Drive Program (multiple issues)
+ Z40: REP HDD (iMac 09/10)
+ Z43: REP HDD (iMac 09/10) (multi-issue)
+ Z50: REP SSD (MacBook Air 2012)
+ Z51: REP SSD (MacBook Air 2012) (multi-issue)
+ Z66: REP 3TB HDD (iMac 2012)
+ Z67: REP 3TB HDD (iMac 2012) (Multi Issue)
+
+"9":
+ category: Communication/Networking Devices (Airport / Bluetooth)
+ symptoms:
+ N01: No Power/Dead Unit
+ N02: TimeCapsule - Internal HDD Not Mounting/Seen
+ N03: Overheating/Fan in Full Speed
+ N04: No/Poor WiFi Signal
+ N05: Backup Issues
+ N06: Can't Configure or Upgrade Firmware
+ N07: Amber LED Flashes
+ N08: USB Connection Issue/Ext USB Devices
+ N09: Random Disconnect/Network Connection Issues
+ N10: Wireless Distribution Setup Issue
+ N11: Audio Issue
+ N13: Kernel Panic/Freeze
+ N14: Performance Issue/Slow Connection
+ N15: Bluetooth Issue
+ N16: Modem - Defective
+ N17: Mechanical Damage/Cosmetic Issues
+ N18: Airport Card - Not Recognized
+ N19: Can't Connect
+ N20: Firmware Update/Restore Issue
+ N21: Cosmetic Issue
+ N85: Unusually Hot - Overheat
+ N90: Liquid Spill/LSI Tripped
+ N99: Un-Categorized Symptom
+ Z13: Time Capsule Power Supply
+ Z14: TC PS Multiple Issues
+
+"8":
+ category: Multi-function Devices (Apple TV, future devices)
+ symptoms:
+ T01: No Power/Dead Unit
+ T02: AppleTV - No Video Output
+ T03: AppleTV - Distorted Video
+ T04: AppleTV - No/Poor Wireless Signal
+ T05: AppleTV - Won't Sync
+ T06: AppleTV - No Audio
+ T07: AppleTV - No Audio in HDMI
+ T08: Won't Boot Up
+ T09: System Hang/Freeze Up
+ T10: Distorted or Cracking Audio
+ T11: Unusually Hot - Overheat
+ T13: Mechanical/Cosmetic Issue
+ T14: Ethernet Connectivity Issue
+ T15: Wireless (Wi-Fi) Connectivity Issue
+ T16: Cosmetic Issue
+ T90: Liquid Spill/LSI Tripped
+ T99: Un-Categorized Symptom
+ Z44: REP Apple TV (2012) Wi-Fi Issue
+
+"4":
+ category: Input Devices (Standalone and Built-In)
+ symptoms:
+ K01: Specific Key(s) Do Not Work
+ K02: No Mouse/Trackpad Response
+ K03: Built-In Keyboard Locks Up
+ K04: Wrong Keyboard Language
+ K05: Sticky Keys
+ K06: Defective Mouse Jogball
+ K07: Wireless Input Device - Can't Pair
+ K08: Wireless Input Device - Lost Connection
+ K09: Wireless Keyboard - No Green LED/No Power
+ K10: Built-In Keyboard - No/Dim Backlight
+ K11: Built-In Keyboard - Not Recognized
+ K12: Trackpad Cursor Not Tracking Properly
+ K13: Trackpad Click Not Recognized
+ K14: Mouse Clicking Issue
+ K15: Device Not Recognized
+ K16: Mechanical/Physical Damage
+ K17: Key Caps - Wrong/Missing/Fall Off
+ K18: Touch/Multi-Touch Gesture Issue
+ K19: Power Button Issue
+ K20: Power Issue, Not due to Power Button
+ K21: Cosmetic Issue
+ K22: Port Functionality Issue
+ K23: Trackpad Cursor Not Responding
+ K24: Trackpad Requires High Click Force
+ K25: Trackpad Click Oversensitive
+ K26: Mouse Issue
+ K27: Key(s) Missing/Falling Off
+ K28: Backlight Uneven Across Keyboard
+ K90: Liquid Spill/LSI Tripped
+ K99: Un-Categorized Symptom
+ Z07: MB Top Case Cracking
+ Z08: MB Top Case Cracking (Multiple Issues)
+
+"2":
+ category: Displays
+ symptoms:
+ L01: No Power/Power Light Issue
+ L02: Incorrect Colors Or Tinting
+ L03: Has Power/Blank/No Video
+ L04: Distorted/Blurred/Non-Focus Video
+ L05: Vertical/Horizontal Lines
+ L06: Full Screen Flicker/Flash
+ L07: Can't Control Brightness
+ L09: No Backlight/Has Video
+ L10: Can't Change Resolution
+ L11: Built In Audio Device Problem
+ L14: Connector/Port/Cable Issue
+ L15: Sleep Function Not Working
+ L16: Wireless Function Not Working
+ L17: Camera Image/Detect Issue
+ L18: Mechanical/Physical Damages
+ L19: Cosmetic Defects
+ L20: Dead Pixels/Foreign Material
+ L21: Bad Spots (Mura)
+ L22: Wake Function Issues
+ L23: Bluetooth Function Not Working
+ L24: Clamshell Misalignment
+ L25: Image Sticking/Ghost
+ L26: Horizontal Lines Or Bands
+ L27: Vertical Lines Or Bands
+ L90: Liquid Spill/LSI Tripped
+ L99: Un-Categorized Symptom
+ Z09: MBAir Hinge Cracking
+ Z10: MBAir Hinge Cracking (Multiple Issues)
+ Z35: LCD Contamination
+
+"6":
+ category: Power Adapter/Supply/Battery
+ symptoms:
+ P01: No Power/Dead Unit
+ P02: PSupply Causes Unexpected Reset/Shutdown
+ P03: No LED/LED Indicated Errors
+ P04: Noise/Hum/Vibration
+ P05: Audio Alarm/Prefailure Notice
+ P06: Power Supply - Fan Not Working/Noisy
+ P07: Wrong Voltage Selector
+ P08: Burnt Smell/Odor
+ P09: Battery Runtime Too Short, Fails Diag
+ P10: Battery Won't Charge At All
+ P11: Battery - Not Recognized
+ P12: Battery Recognized-Won't Run Unit
+ P13: Battery - Leakage/Swollen
+ P14: Adapter - Won't Run on AC alone
+ P15: Adapter Pins Stuck/Broken/Burnt
+ P16: Mechanical - Connector/Cable/Duckhead Damaged
+ P17: Unusually Hot - Overheat
+ P18: Out-of-Warranty Battery Replacement
+ P19: Battery Diagnostic Reported Failure
+ P21: Cosmetic Issue
+ P22: Battery Runtime Too Short, Passes Diag
+ P23: Adapter No Power/Not Damaged
+ P90: Liquid Spill/LSI Tripped
+ P99: Un-Categorized Symptom
+ Z25: Battery single part repair
+ Z26: Battery multi-part repair
+
+"1":
+ category: Other Devices (Internal Cables, Memory Module, etc.)
+ symptoms:
+ X01: Memory Caused Kernel Panic
+ X02: Memory Caused No Boot
+ X03: Cables - Defective
+ X04: Remote - Inoperable
+ X05: Remote - Battery Life Too Short
+ X06: Memory Module (RAM) - Issues
+ X08: Internal Speaker - No Audio
+ X09: Internal Speaker - Distorted Audio/Sound
+ X10: Thermal Module Defective
+ X12: Enclosure - Defective Latch/Hinge
+ X13: Enclosure - Mechanical/Cosmetic Damaged
+ X14: Enclosure - Reset/Power Button Stuck
+ X15: Enclosure - Wobble/Uneven
+ X17: Remote - Specific Button Not Working
+ X19: Microphone - Defective
+ X20: Camera - Video/Image Distortion
+ X21: Camera - No Video
+ X22: Fan Dead
+ X23: Fan Sound Abnormal (tick, whine, grind)
+ X24: Interface Card/Cage Issue
+ X26: Damaged Smart Cable
+ X27: Thunderbolt Firmware Update
+ X90: Liquid Spill/LSI Tripped
+ X99: Un-Categorized Symptom
+ Z21: Bottom Case Delamination
+ Z22: Bottom Case Delamination multiple issues
+
+"G":
+ category: Beats products
+ symptoms:
+ G1A: Audio - Sound quality issue
+ G1B: Audio - Sound input issue
+ G1C: Audio - Headset jack
+ G1D: Microphone issue
+ G1E: Noise Cancellation (ANC) issue
+ G2A: Controls not responding
+ G3A: Liquid damage
+ G3B: Enclosure Physical Damage
+ G3C: Other Physical Damage
+ G3D: Battery Physical Damage
+ G3E: Speaker Physical Damage
+ G4A: Connectivity - Bluetooth
+ G4B: Connectivity - NFC
+ G5A: Will not power up / dead / no power
+ G5B: Unexpected power off / reboot
+ G5C: Battery charge issue
+ G5D: Battery runtime life issue
+ G5E: Issue with power adapter
+ G5F: Firmware/SW apply fail
+ G5G: USB Charge-out issue
+ G6A: Issue with Accessory
+ G7A: Unusually hot
+ G8A: Sensor/indicator issue
diff --git a/servo/fixtures/initial_data.json b/servo/fixtures/initial_data.json
new file mode 100644
index 0000000..5caf88d
--- /dev/null
+++ b/servo/fixtures/initial_data.json
@@ -0,0 +1,10 @@
+[
+ {
+ "model": "servo.Location",
+ "pk": 1,
+ "fields": {
+ "title": "Default Location",
+ "timezone": "Europe/Helsinki"
+ }
+ }
+]
diff --git a/servo/forms/__init__.py b/servo/forms/__init__.py
new file mode 100644
index 0000000..f41490b
--- /dev/null
+++ b/servo/forms/__init__.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from base import *
+from orders import *
+from notes import *
+from devices import *
+from product import *
+from repairs import *
+from .checkin import *
diff --git a/servo/forms/account.py b/servo/forms/account.py
new file mode 100644
index 0000000..775069e
--- /dev/null
+++ b/servo/forms/account.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django import forms
+from django.utils.translation import ugettext as _
+
+from servo.forms.base import BaseForm, BaseModelForm
+from servo.models.account import User
+
+
+class ProfileForm(BaseModelForm):
+ # User Profile form for users
+ class Meta:
+ model = User
+ fields = (
+ "location",
+ "photo",
+ "locale",
+ "queues",
+ "region",
+ "timezone",
+ "should_notify",
+ "notify_by_email",
+ "autoprint",
+ "tech_id",
+ "gsx_userid",
+ "gsx_password",
+ "gsx_poprefix",
+ )
+ widgets = {
+ 'gsx_password': forms.PasswordInput,
+ 'queues': forms.CheckboxSelectMultiple
+ }
+
+ password1 = forms.CharField(
+ widget=forms.PasswordInput,
+ required=False,
+ label=_("Password")
+ )
+ password2 = forms.CharField(
+ widget=forms.PasswordInput,
+ required=False,
+ label=_("Confirmation")
+ )
+
+ def clean(self):
+ cd = super(ProfileForm, self).clean()
+
+ if cd.get('gsx_password') == "":
+ del cd['gsx_password']
+
+ cd['tech_id'] = cd['tech_id'].upper()
+
+ if cd.get('password1'):
+ if cd['password1'] != cd['password2']:
+ raise forms.ValidationError(_("Password and confirmation do not match!"))
+
+ return cd
+
+ def clean_photo(self):
+ photo = self.cleaned_data.get('photo')
+ if photo and photo.size > 1*1024*1024:
+ raise forms.ValidationError(_('File size of photo is too large'))
+
+ return photo
+
+
+class RegistrationForm(BaseForm):
+ first_name = forms.CharField(label=_("First Name"))
+ last_name = forms.CharField(label=_("Last Name"))
+ email = forms.EmailField(label=_("Email Address"))
+ password = forms.CharField(widget=forms.PasswordInput, label=_("Password"))
+
+
+class LoginForm(BaseForm):
+ username = forms.CharField(
+ widget=forms.TextInput(attrs={'placeholder': _('Username')})
+ )
+ password = forms.CharField(
+ widget=forms.PasswordInput(attrs={'placeholder': _('Password')})
+ )
diff --git a/servo/forms/admin.py b/servo/forms/admin.py
new file mode 100644
index 0000000..42a0b0d
--- /dev/null
+++ b/servo/forms/admin.py
@@ -0,0 +1,557 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import io
+
+from django import forms
+from django import template
+from django.utils.translation import ugettext as _
+from django.contrib.auth.models import Permission
+
+from servo.forms.base import BaseForm, BaseModelForm
+from servo.forms.account import ProfileForm
+
+from servo.models.common import *
+from servo.models.queue import *
+from servo.models import User, Group, Checklist
+
+
+class UserUploadForm(forms.Form):
+ datafile = forms.FileField()
+ location = forms.ModelChoiceField(queryset=Location.objects.all())
+ queues = forms.ModelMultipleChoiceField(
+ queryset=Queue.objects.all()
+ )
+ group = forms.ModelChoiceField(queryset=Group.objects.all())
+ queues = forms.ModelMultipleChoiceField(
+ queryset=Queue.objects.all()
+ )
+
+ def save(self, **kwargs):
+ users = []
+ string = u''
+ cd = self.cleaned_data
+ data = cd['datafile'].read()
+
+ for i in ('utf-8', 'latin-1',):
+ try:
+ string = data.decode(i)
+ except:
+ pass
+
+ if not string:
+ raise ValueError(_('Unsupported file encoding'))
+
+ sio = io.StringIO(string, newline=None)
+
+ for l in sio.readlines():
+ cols = l.strip().split("\t")
+ if len(cols) < 2:
+ continue # Skip empty rows
+
+ user = User(username=cols[2])
+ user.first_name = cols[0]
+ user.last_name = cols[1]
+ user.email = cols[3]
+ user.set_password(cols[4])
+ user.save()
+
+ user.location = cd['location']
+ user.timezone = user.location.timezone
+ user.groups.add(cd['group'])
+ user.queues = cd['queues']
+ user.save()
+
+ users.append(user)
+
+ return users
+
+
+class GsxAccountForm(forms.ModelForm):
+ class Meta:
+ model = GsxAccount
+ exclude = []
+ widgets = {'password': forms.PasswordInput}
+
+ def clean(self):
+ cd = super(GsxAccountForm, self).clean()
+ # Don't save empty passwords
+ if cd['password'] == '':
+ del cd['password']
+
+ return cd
+
+
+class GroupForm(forms.ModelForm):
+ class Meta:
+ exclude = []
+ model = Group
+
+ user_set = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_('Group members'),
+ queryset=User.active.all(),
+ widget=forms.CheckboxSelectMultiple
+ )
+
+ permissions = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_('Permissions'),
+ widget=forms.CheckboxSelectMultiple,
+ queryset=Permission.objects.filter(content_type__app_label='servo')
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(GroupForm, self).__init__(*args, **kwargs)
+ if self.instance.pk:
+ user_ids = [u.pk for u in self.instance.user_set.all()]
+ self.fields['user_set'].initial = user_ids
+
+ def save(self, *args, **kwargs):
+ group = super(GroupForm, self).save(*args, **kwargs)
+ group.user_set.clear()
+ for u in self.cleaned_data['user_set']:
+ group.user_set.add(u)
+ return group
+
+
+class ChecklistForm(BaseModelForm):
+ class Meta:
+ model = Checklist
+ exclude = []
+ widgets = {'queues': forms.CheckboxSelectMultiple}
+
+
+class LocationForm(BaseModelForm):
+ class Meta:
+ model = Location
+ exclude = []
+ widgets = {'gsx_accounts': forms.CheckboxSelectMultiple}
+
+ def save(self, **kwargs):
+ from django.db.utils import IntegrityError
+
+ try:
+ location = super(LocationForm, self).save(**kwargs)
+ except IntegrityError:
+ msg = _('A location with that name already exists')
+ self._errors['title'] = self.error_class([msg])
+ raise forms.ValidationError(msg)
+
+ return location
+
+
+class QueueForm(BaseModelForm):
+
+ gsx_soldto = forms.ChoiceField(required=False, choices=())
+ users = forms.ModelMultipleChoiceField(queryset=User.active.all(),
+ widget=forms.CheckboxSelectMultiple,
+ required=False)
+
+ class Meta:
+ model = Queue
+ exclude = ('statuses',)
+ widgets = {
+ 'description' : forms.Textarea(attrs={'rows': 4}),
+ 'keywords' : forms.Textarea(attrs={'rows': 4}),
+ 'locations' : forms.CheckboxSelectMultiple,
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(QueueForm, self).__init__(*args, **kwargs)
+ self.fields['gsx_soldto'].choices = GsxAccount.get_soldto_choices()
+
+ if "instance" in kwargs:
+ queue = kwargs['instance']
+ queryset = QueueStatus.objects.filter(queue=queue)
+ self.fields['status_created'].queryset = queryset
+ self.fields['status_assigned'].queryset = queryset
+ self.fields['status_products_ordered'].queryset = queryset
+ self.fields['status_products_received'].queryset = queryset
+ self.fields['status_repair_completed'].queryset = queryset
+ self.fields['status_dispatched'].queryset = queryset
+ self.fields['status_closed'].queryset = queryset
+
+
+class StatusForm(BaseModelForm):
+ class Meta:
+ model = Status
+ exclude = []
+ widgets = {
+ 'site': forms.HiddenInput,
+ 'limit_green': forms.TextInput(attrs={'class': 'input-mini'}),
+ 'limit_yellow': forms.TextInput(attrs={'class': 'input-mini'}),
+ }
+
+class UserForm(ProfileForm):
+ def clean_username(self):
+ reserved = (
+ 'admin',
+ 'orders',
+ 'sales',
+ 'devices',
+ 'customers',
+ 'notes',
+ 'api',
+ 'checkin',
+ 'feedback',
+ )
+ username = self.cleaned_data.get('username')
+ if username in reserved:
+ raise forms.ValidationError(_(u'"%s" cannot be used as a username') % username)
+
+ return username
+
+ class Meta:
+ model = User
+ fields = (
+ "first_name",
+ "last_name",
+ "username",
+ "email",
+ "is_active",
+ "groups",
+ "is_staff",
+ "location",
+ "locations",
+ "locale",
+ "queues",
+ "region",
+ "timezone",
+ "tech_id",
+ "gsx_userid",
+ "customer",
+ "gsx_poprefix",
+ )
+ widgets = {
+ 'locations': forms.CheckboxSelectMultiple,
+ 'queues': forms.CheckboxSelectMultiple
+ }
+
+class TemplateForm(BaseModelForm):
+ class Meta:
+ model = Template
+ exclude = []
+ widgets = {
+ 'title': forms.TextInput(attrs={'class': 'span12'}),
+ 'content': forms.Textarea(attrs={'class': 'span12'})
+ }
+
+ def clean_content(self):
+ content = self.cleaned_data.get('content')
+ try:
+ template.Template(content)
+ except template.TemplateSyntaxError, e:
+ raise forms.ValidationError(_('Syntax error in template: %s') % e)
+
+ return content
+
+
+class SettingsForm(BaseForm):
+ # Servo's general System Settings form
+ company_name = forms.CharField(label=_('Company Name'))
+ company_logo = forms.ImageField(
+ label=_('Company Logo'),
+ required=False,
+ help_text=_('Company-wide logo to use in print templates')
+ )
+
+ terms_of_service = forms.CharField(
+ required=False,
+ label=_('Terms of Service'),
+ widget=forms.Textarea(attrs={'class': 'span10'}),
+ help_text=_('These terms will be added to your work confirmations and public check-in site.')
+ )
+
+ autocomplete_repairs = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_("Autocomplete GSX repairs"),
+ help_text=_("Complete the GSX repair when closing a Service Order")
+ )
+
+ # start checkin fields
+ checkin_user = forms.ModelChoiceField(
+ required=False,
+ label=_('User Account'),
+ queryset=User.active.all(),
+ help_text=_('User account to use for the public check-in service'),
+ )
+ checkin_group = forms.ModelChoiceField(
+ required=False,
+ label=_('Group'),
+ queryset=Group.objects.all(),
+ help_text=_('Users to choose from in the check-in interface'),
+ )
+ checkin_checklist = forms.ModelChoiceField(
+ required=False,
+ label=_('Checklist'),
+ queryset=Checklist.objects.filter(enabled=True),
+ help_text=_('Checklist to show during check-in'),
+ )
+ checkin_queue = forms.ModelChoiceField(
+ required=False,
+ label=_('Queue'),
+ queryset=Queue.objects.all(),
+ help_text=_('Orders created through the check-in interface will go into this queue'),
+ )
+ checkin_timeline = forms.BooleanField(
+ initial=False,
+ required=False,
+ label=_('Show timeline'),
+ help_text=_('Show status timeline on public repair status page'),
+ )
+ checkin_password = forms.BooleanField(
+ initial=False,
+ required=False,
+ label=_('Show password'),
+ help_text=_('Make checkin device password field readable'),
+ )
+ checkin_report_checklist = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_('Show checklist results'),
+ help_text=_('Show checklist results in order confirmation'),
+ )
+
+ checkin_require_password = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_('Require device password'),
+ )
+ checkin_require_condition = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_('Require device condition'),
+ )
+
+ # end checkin fields
+
+ currency = forms.ChoiceField(
+ label=_('Currency'),
+ choices=(
+ ('DKK', 'DKK'),
+ ('EUR', 'EUR'),
+ ('GBP', 'GBP'),
+ ('SEK', 'SEK'),
+ ('USD', 'USD'),
+ ),
+ initial='EUR'
+ )
+
+ gsx_account = forms.ModelChoiceField(
+ required=False,
+ label=_('Default GSX account'),
+ queryset=GsxAccount.objects.all(),
+ help_text=_('Use this GSX account before and order is assigned to a queue')
+ )
+
+ pct_margin = forms.CharField(
+ required=False,
+ max_length=128,
+ label=_('Margin %'),
+ help_text=_('Default margin for new products')
+ )
+
+ pct_vat = forms.DecimalField(
+ max_digits=4,
+ required=False,
+ label=_('VAT %'),
+ help_text=_('Default VAT for new products')
+ )
+
+ shipping_cost = forms.DecimalField(
+ max_digits=4,
+ required=False,
+ label=_('Shipping Cost'),
+ help_text=_('Default shipping cost for new products')
+ )
+
+ track_inventory = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_('Track inventory'),
+ help_text=_('Unchecking this will disable tracking product amounts in your inventory')
+ )
+
+ imap_host = forms.CharField(
+ label=_('IMAP server'),
+ max_length=128,
+ required=False
+ )
+ imap_user = forms.CharField(
+ label=_('Username'),
+ max_length=128,
+ required=False
+ )
+ imap_password = forms.CharField(
+ max_length=128,
+ label=_('Password'),
+ widget=forms.PasswordInput(),
+ required=False
+ )
+ imap_ssl = forms.BooleanField(label=_('Use SSL'), initial=True, required=False)
+ imap_act = forms.ModelChoiceField(
+ required=False,
+ label=_('User Account'),
+ queryset=User.active.all(),
+ help_text=_('User account to use when creating notes from messages'),
+ )
+
+ default_sender = forms.ChoiceField(
+ required=False,
+ label=_('Default Sender'),
+ choices=(
+ ('user', _("User")),
+ ('location', _("Location")),
+ ('custom', _("Custom..."))
+ ),
+ help_text=_('Select the default sender address for outgoing emails')
+ )
+ default_sender_custom = forms.EmailField(
+ label=' ',
+ required=False,
+ widget=forms.TextInput(attrs={
+ 'placeholder': 'user@example.com', 'disabled': 'disabled'
+ })
+ )
+ default_subject = forms.CharField(
+ max_length=128,
+ required=False,
+ label=_('Default subject')
+ )
+ smtp_host = forms.CharField(
+ max_length=128,
+ required=False,
+ label=_('SMTP server')
+ )
+ smtp_user = forms.CharField(max_length=128, required=False, label=_('Username'))
+ smtp_password = forms.CharField(
+ max_length=128,
+ required=False,
+ label=_('Password'),
+ widget=forms.PasswordInput()
+ )
+ smtp_ssl = forms.BooleanField(initial=True, required=False, label=_('Use SSL'))
+
+ sms_gateway = forms.ChoiceField(
+ label=_('SMS Gateway'),
+ choices=(
+ ('builtin', _('Built-in')),
+ ('hqsms', 'HQSMS'),
+ ('http', 'HTTP'),
+ ('smtp', 'SMTP'),
+ ('jazz', 'SMSjazz'),
+ ),
+ initial='http',
+ required=False)
+ sms_smtp_address = forms.EmailField(required=False, label=_('Email address'))
+ sms_http_url = forms.CharField(
+ max_length=128,
+ label=_('URL'),
+ required=False,
+ help_text=_('SMS Server URL'),
+ initial='http://example.com:13013/cgi-bin/sendsms'
+ )
+ sms_http_user = forms.CharField(max_length=128, label=_('Username'), required=False)
+ sms_http_password = forms.CharField(
+ max_length=128,
+ required=False,
+ label=_('Password'),
+ widget=forms.PasswordInput()
+ )
+ sms_http_sender = forms.CharField(
+ max_length=128,
+ required=False,
+ label=_('Sender')
+ )
+ sms_http_ssl = forms.BooleanField(
+ required=False,
+ label=_('Use SSL'),
+ initial=True
+ )
+ notify_location = forms.BooleanField(
+ required=False,
+ initial=True,
+ label=_('Notify locations'),
+ help_text=_("Daily reports will be sent to the location's email address")
+ )
+ notify_address = forms.EmailField(
+ required=False,
+ label=_('Email address'),
+ help_text=_("Send daily reports to this email address")
+ )
+
+ def clean_notify_address(self, *args, **kwargs):
+ """
+ Only validate notify_address if it was actually given
+ """
+ from django.core.validators import validate_email
+ address = self.cleaned_data.get('notify_address')
+
+ if len(address):
+ validate_email(address)
+
+ return address
+
+ def clean_pct_margin(self, *args, **kwargs):
+ margin = self.cleaned_data.get('pct_margin')
+ if re.match('^\d[\-=;\d]*\d$', margin):
+ return margin
+
+ raise forms.ValidationError(_('Invalid margin %'))
+
+ def save(self, *args, **kwargs):
+ config = dict()
+
+ if self.cleaned_data.get('company_logo'):
+ f = self.cleaned_data['company_logo']
+ target = 'uploads/logos/%s' % f.name
+ with open(target, 'wb+') as destination:
+ for chunk in f.chunks():
+ destination.write(chunk)
+
+ self.cleaned_data['company_logo'] = 'logos/%s' % f.name
+ else:
+ # @fixme: make the form remember the previous value
+ self.cleaned_data['company_logo'] = Configuration.get_company_logo()
+
+ for k, v in self.cleaned_data.items():
+ field = Configuration.objects.get_or_create(key=k)[0]
+
+ if re.search('password$', k) and v == '':
+ v = field.value # don't save empty passwords
+ if hasattr(v, 'pk'):
+ v = v.pk # so we don't end up with object instances in the cache
+
+ field.value = v or ''
+ field.save()
+ config[k] = v
+
+ cache.set('config', config)
+
+ return config
diff --git a/servo/forms/base.py b/servo/forms/base.py
new file mode 100644
index 0000000..83e2cd4
--- /dev/null
+++ b/servo/forms/base.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+from django import forms
+from django.forms.utils import flatatt
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+
+
+class NullCharField(forms.CharField):
+ def clean(self, value):
+ cleaned = super(NullCharField, self).clean(value)
+ return cleaned if len(cleaned) else None
+
+
+class FullTextArea(forms.CharField):
+ widget = forms.Textarea(attrs={'class': 'span12'})
+
+
+class SearchField(forms.CharField):
+ widget = forms.TextInput(attrs={
+ 'class': 'search-query',
+ 'autocomplete': 'off',
+ 'placeholder': '',
+ })
+
+
+class ChoiceField(forms.ChoiceField):
+ def __init__(self, *args, **kwargs):
+ super(ChoiceField, self).__init__(*args, **kwargs)
+ self.widget.attrs['class'] = 'span12'
+
+
+class TextInput(forms.TextInput):
+ def __init__(self, *args, **kwargs):
+ super(TextInput, self).__init__(*args, **kwargs)
+ self.attrs['class'] = 'span12'
+
+
+class AutocompleteCharField(forms.CharField):
+ widget = forms.TextInput(attrs={
+ 'class' : "input typeahead",
+ 'data-provide' : "typeahead"
+ })
+
+ def __init__(self, values, *args, **kwargs):
+ super(AutocompleteCharField, self).__init__(*args, **kwargs)
+
+ if not type(values) is str:
+ values = json.dumps(list(values))
+
+ self.widget.attrs['data-source'] = values
+
+
+class AutocompleteTextarea(forms.Textarea):
+ def __init__(self, rows=8, choices=None):
+ super(AutocompleteTextarea, self).__init__()
+ self.attrs = {
+ 'rows': rows,
+ 'class': "span12 autocomplete",
+ 'data-source': json.dumps(choices)
+ }
+
+
+class BaseForm(forms.Form):
+ required_css_class = "required"
+
+
+class BaseModelForm(forms.ModelForm):
+ required_css_class = "required"
+
+
+class SearchFieldInput(forms.TextInput):
+
+ def render(self, name, value, attrs=None):
+
+ field = super(SearchFieldInput, self).render(name, value, attrs)
+ final_attrs = self.build_attrs(attrs, name=name)
+
+ output = format_html(u'''
+ <div class="input-group">
+ {1}
+ <span class="input-group-btn">
+ <button class="btn btn-default" type="button"><i class="glyphicon glyphicon-search"></i></button>
+ </span>
+ </div>
+ ''', flatatt(final_attrs), field)
+
+ return mark_safe(output)
+
+
+class DatepickerInput(forms.DateInput):
+ def __init__(self, *args, **kwargs):
+ kwargs['format'] = "%Y-%m-%d"
+ super(DatepickerInput, self).__init__(*args, **kwargs)
+
+ def render(self, name, value, attrs=None):
+
+ date_format = "yyyy-MM-dd"
+
+ if "format" not in self.attrs:
+ attrs['format'] = date_format
+
+ if "data-format" not in self.attrs:
+ attrs['data-format'] = date_format
+
+ field = super(DatepickerInput, self).render(name, value, attrs)
+ final_attrs = self.build_attrs(attrs, name=name)
+
+ output = format_html(u'''
+ <div class="input-append date datepicker" data-provide="datepicker" {0}>
+ {1}
+ <span class="add-on">
+ <i data-time-icon="icon-time" data-date-icon="icon-calendar"></i>
+ </span>
+ </div>
+ ''', flatatt(final_attrs), field)
+
+ return mark_safe(output)
+
+
+class DateTimePickerInput(forms.DateTimeInput):
+ def __init__(self, *args, **kwargs):
+ kwargs['format'] = "%Y-%m-%d %H:%M"
+ super(DateTimePickerInput, self).__init__(*args, **kwargs)
+
+ def render(self, name, value, attrs=None):
+
+ date_format = "yyyy-MM-dd hh:mm"
+
+ if "data-format" not in self.attrs:
+ attrs['data-format'] = date_format
+ if "class" not in self.attrs:
+ attrs['class'] = 'input-medium'
+
+ field = super(DateTimePickerInput, self).render(name, value, attrs)
+ final_attrs = self.build_attrs(attrs, name=name)
+
+ output = format_html(u'''
+ <div class="input-append date datetimepicker" {0}>
+ {1}
+ <span class="add-on">
+ <i data-time-icon="icon-time" data-date-icon="icon-calendar"></i>
+ </span>
+ </div>
+ ''', flatatt(final_attrs), field)
+
+ return mark_safe(output)
diff --git a/servo/forms/checkin.py b/servo/forms/checkin.py
new file mode 100644
index 0000000..732b871
--- /dev/null
+++ b/servo/forms/checkin.py
@@ -0,0 +1,303 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+import phonenumbers
+
+from django import forms
+from datetime import date
+from django.conf import settings
+from django_countries import countries
+from django.core.validators import RegexValidator
+from django.utils.translation import ugettext as _
+from django.forms.extras.widgets import SelectDateWidget
+
+from servo.validators import apple_sn_validator, phone_validator, file_upload_validator
+
+from servo.forms.base import SearchFieldInput
+from servo.models import (Configuration,
+ Device, Attachment, Location, Customer,)
+
+
+# Generate list of years for purchase date picker
+y = date.today().year
+YEARS = [x+1 for x in xrange(y-7, y)]
+
+
+def get_checkin_locations(user):
+ from servo.models import User
+ if user.is_authenticated():
+ return user.locations.all()
+ else:
+ user_id = Configuration.conf('checkin_user')
+ return User.objects.get(pk=user_id).locations.all()
+
+
+class ConfirmationForm(forms.Form):
+ confirm = forms.BooleanField(required=False)
+
+
+class DeviceForm(forms.ModelForm):
+
+ required_css_class = 'required'
+
+ purchase_country = forms.ChoiceField(
+ label=_('Country'),
+ choices=countries,
+ initial=settings.INSTALL_COUNTRY
+ )
+ accessories = forms.CharField(
+ required=False,
+ label=_('Accessories'),
+ widget=forms.Textarea(attrs={'class': 'span12', 'rows': 3}),
+ help_text=_("Please list here any accessories you'd like to check in with your device (cables, power adapters, bags, etc)")
+ )
+
+ pop = forms.FileField(
+ required=False,
+ label=_('Proof of Purchase'),
+ validators=[file_upload_validator],
+ help_text=_('Proof of Purchase is required when setting purchase date manually')
+ )
+
+ condition = forms.CharField(
+ label=_('Condition of device'),
+ required=False,
+ widget=forms.Textarea(attrs={'class': 'span12', 'rows': 3}),
+ help_text=_("Please describe the condition of the device")
+ )
+
+ class Meta:
+ model = Device
+ fields = (
+ 'description',
+ 'sn',
+ 'imei',
+ 'purchased_on',
+ 'purchase_country',
+ 'username',
+ 'password',
+ )
+ widgets = {
+ 'sn' : SearchFieldInput(),
+ 'password' : forms.PasswordInput(),
+ 'username' : forms.TextInput(),
+ 'purchased_on' : SelectDateWidget(years=YEARS),
+ 'warranty_status' : forms.Select(attrs={'readonly': 'readonly'}),
+ }
+
+ def __init__(self, *args, **kwargs):
+
+ super(DeviceForm, self).__init__(*args, **kwargs)
+
+ if Configuration.false('checkin_require_password'):
+ self.fields['password'].required = False
+
+ if Configuration.true('checkin_require_condition'):
+ self.fields['condition'].required = True
+
+ if kwargs.get('instance'):
+ prod = gsxws.Product('')
+ prod.description = self.instance.description
+
+ if prod.is_ios:
+ self.fields['password'].label = _('Passcode')
+
+ if not prod.is_ios:
+ del(self.fields['imei'])
+ if not prod.is_mac:
+ del(self.fields['username'])
+
+ if Configuration.true('checkin_password'):
+ self.fields['password'].widget = forms.TextInput(attrs={'class': 'span12'})
+
+
+class CustomerForm(forms.Form):
+
+ from django.utils.safestring import mark_safe
+
+ required_css_class = 'required'
+
+ fname = forms.CharField(
+ label=_('First name'),
+ #initial='Filipp'
+ )
+ lname = forms.CharField(
+ label=_('Last name'),
+ #initial='Lepalaan'
+ )
+
+ company = forms.CharField(
+ required=False,
+ label=_('Company (optional)')
+ )
+ email = forms.EmailField(
+ label=_('Email address'),
+ widget=forms.TextInput(attrs={'class': 'span12'}),
+ #initial='filipp@fps.ee'
+ )
+ phone = forms.CharField(
+ label=_('Phone number'),
+ validators=[phone_validator],
+ #initial='12345678790'
+ )
+ address = forms.CharField(
+ label=_('Address'),
+ #initial='Example street'
+ )
+ country = forms.ChoiceField(label=_('Country'),
+ choices=Customer.COUNTRY_CHOICES,
+ initial=settings.INSTALL_COUNTRY)
+ city = forms.CharField(
+ label=_('City'),
+ #initial='Helsinki'
+ )
+ postal_code = forms.CharField(
+ label=_('Postal Code'),
+ #initial='000100'
+ )
+ checkin_location = forms.ModelChoiceField(
+ empty_label=None,
+ label=_(u'Check-in location'),
+ queryset=Location.objects.all(),
+ widget=forms.Select(attrs={'class': 'span12'}),
+ help_text=_('Choose where you want to leave the device')
+ )
+ checkout_location = forms.ModelChoiceField(
+ empty_label=None,
+ label=_(u'Check-out location'),
+ queryset=Location.objects.all(),
+ widget=forms.Select(attrs={'class': 'span12'}),
+ help_text=_('Choose where you want to pick up the device')
+ )
+ TERMS = _('I agree to the <a href="/checkin/terms/" target="_blank">terms of service.</a>')
+ agree_to_terms = forms.BooleanField(initial=False, label=mark_safe(TERMS))
+
+ notify_by_sms = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_('Notify by SMS')
+ )
+ notify_by_email = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_('Notify by Email')
+ )
+
+ def clean_fname(self):
+ v = self.cleaned_data.get('fname')
+ return v.capitalize()
+
+ def clean_lname(self):
+ lname = self.cleaned_data.get('lname')
+ return lname.capitalize()
+
+ def __init__(self, request, *args, **kwargs):
+
+ super(CustomerForm, self).__init__(*args, **kwargs)
+ user = request.user
+
+ location = request.session['checkin_location']
+ locations = get_checkin_locations(user)
+
+ self.fields['checkin_location'].queryset = locations
+ self.fields['checkin_location'].initial = location
+ self.fields['checkout_location'].queryset = locations
+ self.fields['checkout_location'].initial = location
+
+ if request.user.is_authenticated():
+ del(self.fields['agree_to_terms'])
+ self.fields['phone'].widget = SearchFieldInput()
+
+
+class AppleSerialNumberForm(forms.Form):
+ sn = forms.CharField(
+ min_length=8,
+ #initial='C34JTVKYDTWF',
+ validators=[apple_sn_validator],
+ label=_(u'Serial number or IMEI')
+ )
+
+ def clean_sn(self):
+ sn = self.cleaned_data.get('sn')
+ return sn.upper()
+
+
+class SerialNumberForm(forms.Form):
+ sn = forms.CharField(
+ min_length=8,
+ initial='C34JTVKYDTWF',
+ label=_(u'Serial number')
+ )
+
+ def clean_sn(self):
+ sn = self.cleaned_data.get('sn')
+ return sn.upper()
+
+
+class StatusCheckForm(forms.Form):
+ code = forms.CharField(
+ min_length=8,
+ label=_('Service Order'),
+ validators=[RegexValidator(regex=r'\d{8}', message=_('Invalid Service Order number'))]
+ )
+
+
+class IssueForm(forms.Form):
+
+ required_css_class = 'required'
+
+ issue_description = forms.CharField(
+ min_length=10,
+ #initial='Does not work very well',
+ label=_(u'Problem description'),
+ widget=forms.Textarea(attrs={'class': 'span12'})
+ )
+ attachment = forms.FileField(
+ required=False,
+ label=_(u'Attachment'),
+ validators=[file_upload_validator],
+ help_text=_(u'Please use this to attach relevant documents')
+ )
+
+ notes = forms.CharField(
+ required=False,
+ label=_(u'Notes'),
+ widget=forms.Textarea(attrs={'class': 'span12'}),
+ help_text=_(u'Will not appear on the print-out')
+ )
+
+
+class QuestionForm(forms.Form):
+ question = forms.CharField(widget=forms.HiddenInput)
+ answer = forms.CharField(widget=forms.HiddenInput)
+
+
+class AttachmentForm(forms.ModelForm):
+ class Meta:
+ model = Attachment
+ exclude = []
+
diff --git a/servo/forms/customer.py b/servo/forms/customer.py
new file mode 100644
index 0000000..45752c9
--- /dev/null
+++ b/servo/forms/customer.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import phonenumbers
+from django import forms
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext as _
+
+from servo.forms.base import BaseModelForm, DatepickerInput
+from servo.models import Customer
+
+
+class CustomerForm(BaseModelForm):
+ class Meta:
+ model = Customer
+ widgets = {
+ 'groups': forms.CheckboxSelectMultiple
+ }
+ exclude = []
+
+ def clean_name(self):
+ name = self.cleaned_data.get('name')
+ return name.strip()
+
+ def clean(self):
+ cd = super(CustomerForm, self).clean()
+
+ phone = cd.get('phone')
+ country = cd.get('country')
+
+ if len(phone) < 1:
+ return cd
+
+ try:
+ phonenumbers.parse(phone, country)
+ except phonenumbers.NumberParseException:
+ msg = _('Enter a valid phone number')
+ self._errors["phone"] = self.error_class([msg])
+
+ return cd
+
+
+class CustomerSearchForm(forms.Form):
+ name__icontains = forms.CharField(
+ required=False,
+ label=_('Name contains')
+ )
+ email__icontains = forms.CharField(
+ required=False,
+ label=_('Email contains')
+ )
+ street_address__icontains = forms.CharField(
+ required=False,
+ label=_('Address contains')
+ )
+ checked_in_start = forms.DateField(
+ required=False,
+ label=_('Checked in between'),
+ widget=DatepickerInput(attrs={'class': "input-small"})
+ )
+ checked_in_end = forms.DateField(
+ required=False,
+ label=mark_safe('&nbsp;'),
+ widget=DatepickerInput(attrs={'class': "input-small"})
+ )
+
+ def clean(self):
+ cd = super(CustomerSearchForm, self).clean()
+
+ for k, v in cd.items():
+ if v not in ['', None]:
+ return cd
+
+ raise forms.ValidationError(_('Please specify at least one parameter'))
+
+
+class CustomerUploadForm(forms.Form):
+ datafile = forms.FileField(label=_('CSV file'))
+ skip_dups = forms.BooleanField(
+ initial=False,
+ required=False,
+ label=_('Skip duplicates'),
+ help_text=_('Skip customers with existing email addresses')
+ )
+
+ def clean_datafile(self):
+ d = self.cleaned_data.get('datafile')
+ if not d.content_type.startswith('text'):
+ raise forms.ValidationError(_('Data file should be in text format'))
+ return d
diff --git a/servo/forms/devices.py b/servo/forms/devices.py
new file mode 100644
index 0000000..3028b52
--- /dev/null
+++ b/servo/forms/devices.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django import forms
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext as _
+
+from servo.models import Tag, Device, Customer
+from servo.forms import DatepickerInput, AutocompleteCharField
+
+product_lines = [(k, x['name']) for k, x in Device.PRODUCT_LINES.items()]
+
+
+class DeviceSearchForm(forms.Form):
+ product_line = forms.MultipleChoiceField(
+ #widget=forms.CheckboxSelectMultiple,
+ choices=product_lines,
+ required=False
+ )
+ warranty_status = forms.MultipleChoiceField(
+ #widget=forms.CheckboxSelectMultiple,
+ choices=Device.WARRANTY_CHOICES,
+ required=False,
+ )
+ date_start = forms.DateField(
+ required=False,
+ label=_('Created between'),
+ widget=DatepickerInput(attrs={'class': 'input-small'})
+ )
+ date_end = forms.DateField(
+ required=False,
+ label=mark_safe('&nbsp;'),
+ widget=DatepickerInput(attrs={'class': 'input-small'})
+ )
+ sn = forms.CharField(required=False, label=_('Serial number contains'))
+
+ def __init__(self, *args, **kwargs):
+ super(DeviceSearchForm, self).__init__(*args, **kwargs)
+ self.fields['description'] = AutocompleteCharField(
+ '/api/device_models/',
+ max_length=128,
+ required=False,
+ label=_('Description contains')
+ )
+
+
+class DeviceForm(forms.ModelForm):
+ class Meta:
+ model = Device
+ exclude = ('spec', 'customers', 'files', 'image_url',
+ 'exploded_view_url', 'manual_url',)
+ widgets = {'purchased_on': DatepickerInput()}
+
+ tags = forms.ModelMultipleChoiceField(
+ queryset=Tag.objects.filter(type='device'),
+ required=False
+ )
+
+
+class DeviceUploadForm(forms.Form):
+ datafile = forms.FileField()
+ customer = forms.ModelChoiceField(
+ queryset=Customer.objects.all(),
+ required=False
+ )
+ do_warranty_check = forms.BooleanField(required=False, initial=True)
+
+
+class DiagnosticsForm(forms.Form):
+ pass
+
diff --git a/servo/forms/invoices.py b/servo/forms/invoices.py
new file mode 100644
index 0000000..eb96f7e
--- /dev/null
+++ b/servo/forms/invoices.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django import forms
+from django.utils.translation import ugettext as _
+
+from servo.forms import DatepickerInput, NullCharField
+from servo.models import Invoice, Payment, Status
+
+
+class PaymentForm(forms.ModelForm):
+ class Meta:
+ model = Payment
+ exclude = []
+ widgets = {
+ 'created_by': forms.HiddenInput,
+ 'method': forms.Select(attrs={'class': 'input-medium'}),
+ 'amount': forms.NumberInput(attrs={'class': 'input-small'})
+ }
+
+
+class InvoiceForm(forms.ModelForm):
+ class Meta:
+ model = Invoice
+ exclude = []
+ widgets = {
+ 'total_net' : forms.TextInput(attrs={'class': 'input-small'}),
+ 'total_tax' : forms.TextInput(attrs={'class': 'input-small'}),
+ 'total_gross' : forms.TextInput(attrs={'class': 'input-small'}),
+ 'customer_name' : forms.TextInput(attrs={'class': 'span12'}),
+ 'customer_email' : forms.TextInput(attrs={'class': 'span12'}),
+ 'customer_phone' : forms.TextInput(attrs={'class': 'span12'}),
+ 'customer_address' : forms.TextInput(attrs={'class': 'span12'}),
+ 'reference' : forms.TextInput(attrs={'class': 'span12'}),
+ }
+ localized_fields = ('total_net', 'total_tax', 'total_gross')
+
+
+class InvoiceSearchForm(forms.Form):
+ state = forms.ChoiceField(
+ required=False,
+ label=_('State is'),
+ choices=(
+ ('', _('Any')),
+ ('OPEN', _('Open')),
+ ('PAID', _('Paid')),
+ ),
+ widget=forms.Select(attrs={'class': 'input-small'})
+ )
+ payment_method = forms.ChoiceField(
+ required=False,
+ label=_('Payment method is'),
+ choices=(('', _('Any')),) + Payment.METHODS,
+ widget=forms.Select(attrs={'class': 'input-medium'})
+ )
+ status_isnot = forms.ModelChoiceField(
+ required=False,
+ label=_('Status is not'),
+ queryset=Status.objects.all(),
+ widget=forms.Select(attrs={'class': 'input-medium'})
+ )
+ start_date = forms.DateField(
+ required=False,
+ label=_('Start date'),
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('Start date')
+ })
+ )
+ end_date = forms.DateField(
+ required=False,
+ label=_('End date'),
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('End date')
+ })
+ )
+ customer_name = NullCharField(
+ required=False,
+ label=_('Customer name contains')
+ )
+ service_order = NullCharField(
+ required=False,
+ label=_('Service Order is')
+ )
diff --git a/servo/forms/notes.py b/servo/forms/notes.py
new file mode 100644
index 0000000..d74a0de
--- /dev/null
+++ b/servo/forms/notes.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+from django import forms
+from gsxws import escalations
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext as _
+
+from servo.models import Note, Escalation, Template
+from servo.forms import BaseModelForm, AutocompleteTextarea, TextInput
+
+
+class NoteForm(BaseModelForm):
+ class Meta:
+ model = Note
+ exclude = []
+ widgets = {
+ 'recipient' : TextInput,
+ 'labels' : forms.CheckboxSelectMultiple,
+ 'order' : forms.HiddenInput,
+ 'parent' : forms.HiddenInput,
+ 'customer' : forms.HiddenInput,
+ 'subject' : TextInput
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(NoteForm, self).__init__(*args, **kwargs)
+ note = kwargs['instance']
+ self.fields['sender'] = forms.ChoiceField(
+ label=_('From'),
+ choices=note.get_sender_choices(),
+ widget=forms.Select(attrs={'class': 'span12'})
+ )
+ self.fields['body'].widget = AutocompleteTextarea(
+ rows=20,
+ choices=Template.templates()
+ )
+
+
+class NoteSearchForm(forms.Form):
+ body = forms.CharField(required=False, label=_('Body contains'))
+ recipient = forms.CharField(required=False, label=_('Recipient contains'))
+ sender = forms.CharField(required=False, label=_('Sender contains'))
+ order_code = forms.CharField(required=False, label=_('Service Order is'))
+
+
+class EscalationForm(BaseModelForm):
+ keys = forms.CharField(required=False)
+ values = forms.CharField(required=False)
+
+ def clean(self):
+ contexts = dict()
+ cd = super(EscalationForm, self).clean()
+ keys = self.data.getlist('keys')
+ values = self.data.getlist('values')
+ for k, v in enumerate(values):
+ if v != '':
+ key = keys[k]
+ contexts[key] = v
+
+ cd['contexts'] = json.dumps(contexts)
+ return cd
+
+ class Meta:
+ model = Escalation
+ fields = ('issue_type', 'status', 'gsx_account', 'contexts',)
+
diff --git a/servo/forms/orders.py b/servo/forms/orders.py
new file mode 100644
index 0000000..82c49da
--- /dev/null
+++ b/servo/forms/orders.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+
+from django import forms
+from django.utils.translation import ugettext as _
+
+from django.utils.safestring import mark_safe
+
+from servo.models.parts import symptom_codes
+from servo.models import Location, Queue, Status, Tag
+from servo.models import User, Invoice, Payment
+
+from servo.models.order import *
+from servo.forms.base import *
+
+
+class BatchProcessForm(forms.Form):
+ orders = forms.CharField(
+ widget=forms.Textarea,
+ label=_("Service order(s)")
+ )
+
+ status = forms.ModelChoiceField(
+ required=False,
+ label=_('Set status to'),
+ queryset=Status.objects.all()
+ )
+ queue = forms.ModelChoiceField(
+ required=False,
+ label=_('Set queue to'),
+ queryset=Queue.objects.all()
+ )
+ sms = forms.CharField(
+ required=False,
+ widget=forms.Textarea,
+ label=_('Send SMS to customer')
+ )
+ email = forms.CharField(
+ required=False,
+ widget=forms.Textarea,
+ label=_('Send E-mail to customer')
+ )
+ note = forms.CharField(
+ required=False,
+ widget=forms.Textarea,
+ label=_('Add note to order')
+ )
+
+
+class FieldsForm(forms.Form):
+ pass
+
+
+class OrderItemForm(forms.ModelForm):
+ class Meta:
+ model = ServiceOrderItem
+ fields = ('title', 'amount', 'price_category',
+ 'price', 'sn', 'kbb_sn', 'imei', 'should_report',
+ 'comptia_code', 'comptia_modifier',)
+ widgets = {
+ 'amount': forms.TextInput(attrs={'class': 'input-mini'}),
+ 'price': forms.TextInput(attrs={'class': 'input-mini'})
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(OrderItemForm, self).__init__(*args, **kwargs)
+
+ if self.instance:
+ product = self.instance.product
+ if product.can_order_from_gsx():
+ CODES = symptom_codes(product.component_code)
+ self.fields['comptia_code'] = forms.ChoiceField(choices=CODES)
+ self.fields['comptia_modifier'] = forms.ChoiceField(
+ choices=gsxws.MODIFIERS,
+ initial="B"
+ )
+
+
+class OrderSearchForm(forms.Form):
+ """
+ Form for searching Service Orders
+ """
+ checkin_location = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_("Checked in at"),
+ queryset=Location.objects.all(),
+ )
+ location = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_("Location is"),
+ queryset=Location.objects.all(),
+ )
+ state = forms.MultipleChoiceField(
+ required=False,
+ label=_("State is"),
+ choices=Order.STATES,
+ )
+ queue = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_("Queue is"),
+ queryset=Queue.objects.all(),
+ )
+ status = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_("Status"),
+ queryset=Status.objects.all(),
+ )
+ created_by = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_("Created by"),
+ queryset=User.active.all(),
+ )
+ assigned_to = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_("Assigned to"),
+ queryset=User.active.all(),
+ )
+ label = forms.ModelMultipleChoiceField(
+ required=False,
+ label=_("Label"),
+ queryset=Tag.objects.filter(type="order"),
+ )
+ color = forms.MultipleChoiceField(
+ choices=(
+ ('green', _("Green")),
+ ('yellow', _("Yellow")),
+ ('red', _("Red")),
+ ('grey', _("Grey")),
+ ),
+ label=_("Color"),
+ required=False,
+ )
+ start_date = forms.DateField(
+ required=False,
+ label=_("Created between"),
+ widget=DatepickerInput(attrs={'class': "input-small"})
+ )
+ end_date = forms.DateField(
+ required=False,
+ label=mark_safe('&nbsp;'),
+ widget=DatepickerInput(attrs={'class': "input-small"})
+ )
diff --git a/servo/forms/product.py b/servo/forms/product.py
new file mode 100644
index 0000000..227a9b3
--- /dev/null
+++ b/servo/forms/product.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+from django import forms
+from django.utils.translation import ugettext as _
+from django.core.exceptions import ValidationError
+
+from servo.models import Location, User, TaggedItem
+from servo.models.purchases import PurchaseOrderItem
+from servo.models.product import Product, ProductCategory
+from servo.forms.base import BaseModelForm, DatepickerInput, TextInput
+
+
+class ProductSearchForm(forms.Form):
+ title = forms.CharField(
+ required=False,
+ label=_('Name contains')
+ )
+ code = forms.CharField(
+ required=False,
+ label=_('Code contains')
+ )
+ description = forms.CharField(
+ required=False,
+ label=_('Description contains')
+ )
+ tag = forms.ModelChoiceField(
+ required=False,
+ label=_('Device model is'),
+ queryset=TaggedItem.objects.none()
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(ProductSearchForm, self).__init__(*args, **kwargs)
+ tags = TaggedItem.objects.filter(content_type__model="product").distinct("tag")
+ self.fields['tag'].queryset = tags
+
+
+class ProductUploadForm(forms.Form):
+ datafile = forms.FileField(label=_("Product datafile"))
+ category = forms.ModelChoiceField(
+ required=False,
+ queryset=ProductCategory.objects.all()
+ )
+
+
+class PartsImportForm(forms.Form):
+ partsdb = forms.FileField(label=_("Parts database file"))
+ import_vintage = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_("Import vintage parts")
+ )
+ update_prices = forms.BooleanField(
+ initial=True,
+ required=False,
+ label=_("Update product prices")
+ )
+
+
+class PurchaseOrderItemEditForm(forms.ModelForm):
+ class Meta:
+ model = PurchaseOrderItem
+ exclude = ('sn',)
+ widgets = {
+ 'product': forms.HiddenInput(),
+ 'code': forms.TextInput(attrs={'class': 'input-small'}),
+ 'amount': forms.TextInput(attrs={'class': 'input-mini'}),
+ 'price': forms.TextInput(attrs={'class': 'input-mini'}),
+ 'title': forms.TextInput(attrs={'class': 'input-xlarge'}),
+ }
+ localized_fields = ('price',)
+
+
+class PurchaseOrderItemForm(forms.ModelForm):
+ class Meta:
+ model = PurchaseOrderItem
+ fields = ('sn', 'amount',)
+ localized_fields = ('price',)
+
+ def clean(self):
+ cleaned_data = super(PurchaseOrderItemForm, self).clean()
+ return cleaned_data
+
+
+class ProductForm(forms.ModelForm):
+ class Meta:
+ model = Product
+ exclude = ('files',)
+ widgets = {
+ 'code': TextInput(),
+ 'title': TextInput(attrs={'class': 'input-xlarge'}),
+ 'categories': forms.CheckboxSelectMultiple(),
+ 'description': forms.Textarea(attrs={'class': 'span12', 'rows': 6}),
+ }
+ localized_fields = (
+ 'price_purchase_exchange',
+ 'pct_margin_exchange',
+ 'price_notax_exchange',
+ 'price_sales_exchange',
+ 'price_purchase_stock',
+ 'pct_margin_stock',
+ 'price_notax_stock',
+ 'price_sales_stock',
+ 'pct_vat',
+ 'shipping',
+ )
+
+ def clean_code(self):
+ code = self.cleaned_data.get('code')
+ if not re.match(r'^[\w\-/]+$', code):
+ raise ValidationError(_('Product code %s contains invalid characters') % code)
+
+ return code
+
+
+class CategoryForm(BaseModelForm):
+ class Meta:
+ model = ProductCategory
+ exclude = []
+
+
+class PurchaseOrderSearchForm(forms.Form):
+ state = forms.ChoiceField(
+ required=False,
+ label=_('State is'),
+ choices=(
+ ('', _('Any')),
+ ('open', _('Open')),
+ ('submitted', _('Submitted')),
+ ('received', _('Received')),
+ ),
+ widget=forms.Select(attrs={'class': 'input-small'})
+ )
+ created_by = forms.ModelChoiceField(
+ required=False,
+ queryset=User.objects.filter(is_active=True)
+ )
+ start_date = forms.DateField(
+ required=False,
+ label=_('Start date'),
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('Start date')
+ })
+ )
+ end_date = forms.DateField(
+ required=False,
+ label=_('End date'),
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('End date')
+ })
+ )
+ reference = forms.CharField(
+ required=False,
+ label=_('Reference contains')
+ )
+
+
+class IncomingSearchForm(forms.Form):
+ """
+ A form for searching incoming products
+ """
+ location = forms.ModelChoiceField(
+ label=_('Location is'),
+ queryset=Location.objects.all(),
+ widget=forms.Select(attrs={'class': 'input-medium'})
+ )
+ ordered_start_date = forms.DateField(
+ label=_('Ordered between'),
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('Start date')
+ })
+ )
+ ordered_end_date = forms.DateField(
+ label='',
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('End date')
+ })
+ )
+ received_start_date = forms.DateField(
+ label=_('Received between'),
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('Start date')
+ })
+ )
+ received_end_date = forms.DateField(
+ label='',
+ widget=DatepickerInput(attrs={
+ 'class': "input-small",
+ 'placeholder': _('End date')
+ })
+ )
+ confirmation = forms.CharField(
+ label=_('Confirmation is')
+ )
+ service_order = forms.CharField(
+ label=_('Service order is')
+ )
+
diff --git a/servo/forms/repairs.py b/servo/forms/repairs.py
new file mode 100644
index 0000000..8459e52
--- /dev/null
+++ b/servo/forms/repairs.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+
+from django import forms
+from django_countries import countries
+
+from django.utils.translation import ugettext as _
+
+from servo.models import User, Repair, Template
+from servo.forms import BaseForm, AutocompleteTextarea, DateTimePickerInput, ChoiceField
+
+
+class GsxCustomerForm(BaseForm):
+ firstName = forms.CharField(max_length=100, label=_('First name'))
+ lastName = forms.CharField(max_length=100, label=_('Last name'))
+ emailAddress = forms.CharField(max_length=100, label=_('Email'))
+ primaryPhone = forms.CharField(max_length=100, label=_('Phone'))
+ addressLine1 = forms.CharField(max_length=100, label=_('Address'))
+ zipCode = forms.CharField(max_length=100, label=_('ZIP Code'))
+ city = forms.CharField(max_length=100, label=_('City'))
+ country = ChoiceField(label=_('Country'), choices=countries)
+ state = ChoiceField(choices=(('ZZ', _('Other')),), initial="ZZ")
+
+
+class GsxComponentForm(forms.Form):
+ def __init__(self, *args, **kwargs):
+ components = kwargs.get('components')
+ del kwargs['components']
+ super(GsxComponentForm, self).__init__(*args, **kwargs)
+ if len(components):
+ components = json.loads(components)
+ for k, v in components.items():
+ self.fields[k] = forms.CharField(label=k, required=True, initial=v)
+
+ def clean(self, *args, **kwargs):
+ super(GsxComponentForm, self).clean(*args, **kwargs)
+ self.json_data = json.dumps(self.cleaned_data)
+
+
+class GsxRepairForm(forms.ModelForm):
+ class Meta:
+ model = Repair
+ exclude = []
+ widgets = {
+ 'device' : forms.HiddenInput(),
+ 'parts': forms.CheckboxSelectMultiple(),
+ 'unit_received_at': DateTimePickerInput(attrs={'readonly': 'readonly'})
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(GsxRepairForm, self).__init__(*args, **kwargs)
+ repair = kwargs['instance']
+ techs = User.techies.filter(location=repair.order.location)
+ c = [(u.tech_id, u.get_full_name()) for u in techs]
+ c.insert(0, ('', '-------------------',))
+ self.fields['tech_id'] = forms.ChoiceField(choices=c,
+ required=False,
+ label=_('Technician'))
+ self.fields['parts'].initial = repair.order.get_parts()
+
+ if not repair.can_mark_complete:
+ del self.fields['mark_complete']
+ del self.fields['replacement_sn']
+
+ choices = Template.templates()
+ for f in ('notes', 'symptom', 'diagnosis'):
+ self.fields[f].widget = AutocompleteTextarea(choices=choices)
+
+ def clean(self, *args, **kwargs):
+ cd = super(GsxRepairForm, self).clean(*args, **kwargs)
+ if self.instance.has_serialized_parts():
+ if cd.get('mark_complete') and not cd.get('replacement_sn'):
+ raise forms.ValidationError(_('Replacement serial number must be set'))
+ return cd
diff --git a/servo/forms/returns.py b/servo/forms/returns.py
new file mode 100644
index 0000000..f2ef014
--- /dev/null
+++ b/servo/forms/returns.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+from django import forms
+from django.utils.translation import ugettext as _
+
+from servo.models import Location, Shipment, ServicePart
+
+
+class ConvertToStockForm(forms.Form):
+ partNumber = forms.CharField(widget=forms.HiddenInput())
+
+
+class GoodPartReturnForm(forms.Form):
+ comptiaModifier = forms.ChoiceField(
+ label=_("Reason"),
+ choices=[
+ ('', _("Select...")),
+ ('A', _("Part not needed")),
+ ('B', _("Duplicated part")),
+ ('C', _("Added wrong part")),
+ ('D', _("Tried to cancel order")),
+ ('E', _("Customer refused order")),
+ ]
+ )
+ comptiaCode = forms.ChoiceField(
+ label=_("Type"),
+ choices=[
+ ('', _("Select...")),
+ ('DIA', _("Diagnostic")),
+ ('UOP', _("Un-Opened")),
+ ]
+ )
+
+
+class DoaPartReturnForm(forms.Form):
+ comptiaCode = forms.ChoiceField(
+ label=_("Symptom Code"),
+ choices=[('', _("Select..."))]
+ )
+ comptiaModifier = forms.ChoiceField(
+ label=_("Symptom Modifier"),
+ choices=gsxws.comptia.MODIFIERS
+ )
+
+ def __init__(self, part, data=None):
+ super(DoaPartReturnForm, self).__init__(data=data)
+ self.fields['comptiaCode'].choices += part.get_comptia_symptoms()
+
+
+class BulkReturnSearchForm(forms.Form):
+ location = forms.ModelChoiceField(
+ label=_('Location'),
+ queryset=Location.objects.all()
+ )
+
+
+class BulkReturnPartForm(forms.ModelForm):
+ class Meta:
+ model = ServicePart
+ widgets = {
+ 'box_number': forms.Select(attrs={'class': 'input-small'}),
+ 'part_number': forms.HiddenInput(),
+ 'part_title': forms.HiddenInput(),
+ 'service_order': forms.HiddenInput(),
+ 'return_order': forms.HiddenInput(),
+ }
+ exclude = []
+
+ def __init__(self, *args, **kwargs):
+ super(BulkReturnPartForm, self).__init__(*args, **kwargs)
+ if 'instance' in kwargs:
+ box_choices = [(0, 'Individual',)]
+ instance = kwargs['instance']
+ # @TODO: This seems like a totally unnecessary hack...
+ # Why can't I just pass the number of options directly to the form?
+ part_count = instance.shipment.servicepart_set.all().count()
+ for x in xrange(1, part_count):
+ box_choices.append((x, x,))
+
+ self.fields['box_number'].widget.choices = box_choices
+
+
+class BulkReturnForm(forms.ModelForm):
+ class Meta:
+ model = Shipment
+ exclude = []
+
diff --git a/servo/lib/__init__.py b/servo/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/lib/__init__.py
diff --git a/servo/lib/middleware.py b/servo/lib/middleware.py
new file mode 100644
index 0000000..b917fa5
--- /dev/null
+++ b/servo/lib/middleware.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import pytz
+from re import compile
+from django.conf import settings
+from django.utils import timezone
+from django.http import HttpResponseRedirect
+
+
+if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
+ EXEMPT_URLS = [compile(expr) for expr in settings.LOGIN_EXEMPT_URLS]
+
+
+class LoginRequiredMiddleware(object):
+ """
+ Middleware that requires a user to be authenticated to view any page other
+ than LOGIN_URL. Exemptions to this requirement can optionally be specified
+ in settings via a list of regular expressions in LOGIN_EXEMPT_URLS (which
+ you can copy from your urls.py).
+
+ Requires authentication middleware and template context processors to be
+ loaded. You'll get an error if they aren't.
+ """
+ def process_request(self, request):
+ assert hasattr(request, 'user'), "The Login Required middleware\
+ requires authentication middleware to be installed. Edit your\
+ MIDDLEWARE_CLASSES setting to insert\
+ 'django.contrib.auth.middlware.AuthenticationMiddleware'. If that doesn't\
+ work, ensure your TEMPLATE_CONTEXT_PROCESSORS setting includes\
+ 'django.core.context_processors.auth'."
+ if not request.user.is_authenticated():
+ path = request.path_info.lstrip('/')
+ if not any(m.match(path) for m in EXEMPT_URLS):
+ return HttpResponseRedirect(settings.LOGIN_URL)
+
+
+class TimezoneMiddleware(object):
+ def process_request(self, request):
+ tzname = request.session.get('django_timezone')
+ if tzname:
+ timezone.activate(pytz.timezone(tzname))
+ else:
+ timezone.deactivate()
diff --git a/servo/lib/shorturl.py b/servo/lib/shorturl.py
new file mode 100644
index 0000000..f93028f
--- /dev/null
+++ b/servo/lib/shorturl.py
@@ -0,0 +1,130 @@
+'''
+Short URL Generator
+===================
+
+Python implementation for generating Tiny URL- and bit.ly-like URLs.
+
+A bit-shuffling approach is used to avoid generating consecutive, predictable
+URLs. However, the algorithm is deterministic and will guarantee that no
+collisions will occur.
+
+The URL alphabet is fully customizable and may contain any number of
+characters. By default, digits and lower-case letters are used, with
+some removed to avoid confusion between characters like o, O and 0. The
+default alphabet is shuffled and has a prime number of characters to further
+improve the results of the algorithm.
+
+The block size specifies how many bits will be shuffled. The lower BLOCK_SIZE
+bits are reversed. Any bits higher than BLOCK_SIZE will remain as is.
+BLOCK_SIZE of 0 will leave all bits unaffected and the algorithm will simply
+be converting your integer to a different base.
+
+The intended use is that incrementing, consecutive integers will be used as
+keys to generate the short URLs. For example, when creating a new URL, the
+unique integer ID assigned by a database could be used to generate the URL
+by using this module. Or a simple counter may be used. As long as the same
+integer is not used twice, the same short URL will not be generated twice.
+
+The module supports both encoding and decoding of URLs. The min_length
+parameter allows you to pad the URL if you want it to be a specific length.
+
+Sample Usage:
+
+>>> import short_url
+>>> url = short_url.encode_url(12)
+>>> print url
+LhKA
+>>> key = short_url.decode_url(url)
+>>> print key
+12
+
+Use the functions in the top-level of the module to use the default encoder.
+Otherwise, you may create your own UrlEncoder object and use its encode_url
+and decode_url methods.
+
+Author: Michael Fogleman
+License: MIT
+Link: http://code.activestate.com/recipes/576918/
+'''
+
+MIN_LENGTH = 4
+DEFAULT_BLOCK_SIZE = 24
+DEFAULT_ALPHABET = 'mn6j2c4rvbpygw9z7hsdaetxuk3fq'
+
+class UrlEncoder(object):
+ def __init__(self, alphabet=DEFAULT_ALPHABET, block_size=DEFAULT_BLOCK_SIZE):
+ self.alphabet = alphabet
+ self.block_size = block_size
+ self.mask = (1 << block_size) - 1
+ self.mapping = range(block_size)
+ self.mapping.reverse()
+ def encode_url(self, n, min_length=MIN_LENGTH):
+ return self.enbase(self.encode(n), min_length)
+ def decode_url(self, n):
+ return self.decode(self.debase(n))
+ def encode(self, n):
+ return (n & ~self.mask) | self._encode(n & self.mask)
+ def _encode(self, n):
+ result = 0
+ for i, b in enumerate(self.mapping):
+ if n & (1 << i):
+ result |= (1 << b)
+ return result
+ def decode(self, n):
+ return (n & ~self.mask) | self._decode(n & self.mask)
+ def _decode(self, n):
+ result = 0
+ for i, b in enumerate(self.mapping):
+ if n & (1 << b):
+ result |= (1 << i)
+ return result
+ def enbase(self, x, min_length=MIN_LENGTH):
+ result = self._enbase(x)
+ padding = self.alphabet[0] * (min_length - len(result))
+ return '%s%s' % (padding, result)
+ def _enbase(self, x):
+ n = len(self.alphabet)
+ if x < n:
+ return self.alphabet[x]
+ return self._enbase(x / n) + self.alphabet[x % n]
+ def debase(self, x):
+ n = len(self.alphabet)
+ result = 0
+ for i, c in enumerate(reversed(x)):
+ result += self.alphabet.index(c) * (n ** i)
+ return result
+
+DEFAULT_ENCODER = UrlEncoder()
+
+def encode(n):
+ return DEFAULT_ENCODER.encode(n)
+
+def decode(n):
+ return DEFAULT_ENCODER.decode(n)
+
+def enbase(n, min_length=MIN_LENGTH):
+ return DEFAULT_ENCODER.enbase(n, min_length)
+
+def debase(n):
+ return DEFAULT_ENCODER.debase(n)
+
+def encode_url(n, min_length=MIN_LENGTH):
+ return DEFAULT_ENCODER.encode_url(n, min_length)
+
+def decode_url(n):
+ return DEFAULT_ENCODER.decode_url(n)
+
+def from_time():
+ from time import time
+ return encode_url(int(time()*1000)).upper()
+
+if __name__ == '__main__':
+ for a in range(0, 200000, 37):
+ b = encode(a)
+ c = enbase(b)
+ d = debase(c)
+ e = decode(d)
+ assert a == e
+ assert b == d
+ c = (' ' * (7 - len(c))) + c
+ print '%6d %12d %s %12d %6d' % (a, b, c, d, e)
diff --git a/servo/lib/ucsv.py b/servo/lib/ucsv.py
new file mode 100644
index 0000000..91b2acf
--- /dev/null
+++ b/servo/lib/ucsv.py
@@ -0,0 +1,49 @@
+"""
+Borrwed from
+http://stackoverflow.com/questions/1846135/python-csv-library-with-unicode-utf-8-support-that-just-works
+"""
+
+import csv
+import codecs
+
+class UnicodeCsvReader(object):
+ def __init__(self, f, encoding="utf-8", **kwargs):
+ self.csv_reader = csv.reader(f, **kwargs)
+ self.encoding = encoding
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ # read and split the csv row into fields
+ row = self.csv_reader.next()
+ # now decode
+ return [unicode(cell, self.encoding) for cell in row]
+
+ @property
+ def line_num(self):
+ return self.csv_reader.line_num
+
+
+class UnicodeDictReader(csv.DictReader):
+ def __init__(self, f, encoding="utf-8", fieldnames=None, **kwargs):
+ csv.DictReader.__init__(self, f, fieldnames=fieldnames, **kwargs)
+ self.reader = UnicodeCsvReader(f, encoding=encoding, **kwargs)
+
+
+def read_excel_file(f):
+ dialect = csv.Sniffer().sniff(codecs.EncodedFile(f, "utf-8").read(1024))
+ #f.open()
+ return UnicodeCsvReader(codecs.EncodedFile(f, "utf-8"),
+ "utf-8", dialect=dialect)
+
+def main():
+ import sys
+ with codecs.open(sys.argv[1], 'rUb') as csvfile:
+ reader = read_excel_file(csvfile)
+ for row in reader:
+ print u', '.join(row)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/servo/locale/da_DK/LC_MESSAGES/django.mo b/servo/locale/da_DK/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e7d09c8
--- /dev/null
+++ b/servo/locale/da_DK/LC_MESSAGES/django.mo
Binary files differ
diff --git a/servo/locale/da_DK/LC_MESSAGES/django.po b/servo/locale/da_DK/LC_MESSAGES/django.po
new file mode 100644
index 0000000..19434b9
--- /dev/null
+++ b/servo/locale/da_DK/LC_MESSAGES/django.po
@@ -0,0 +1,5236 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-08-23 10:41+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms/account.py:36 forms/account.py:70 forms/account.py:78
+#: forms/admin.py:364 forms/admin.py:407 forms/admin.py:435
+#: models/account.py:138 models/common.py:197
+#: templates/devices/get_info.html:33 templates/devices/summary.html:24
+#: templates/orders/devices.html:41
+msgid "Password"
+msgstr ""
+
+#: forms/account.py:41 templates/checkin/confirmation.html:6
+#: templates/orders/repair.html:28 templates/products/receive_item.html:33
+#: templates/purchases/list_pos.html:55 templates/purchases/view_po.html:23
+#: templates/shipments/list_incoming.html:19
+msgid "Confirmation"
+msgstr ""
+
+#: forms/account.py:54
+msgid "Password and confirmation do not match!"
+msgstr ""
+
+#: forms/account.py:61
+msgid "File size of photo is too large"
+msgstr ""
+
+#: forms/account.py:67
+msgid "First Name"
+msgstr ""
+
+#: forms/account.py:68
+msgid "Last Name"
+msgstr ""
+
+#: forms/account.py:69 templates/accounts/print_calendar.html:23
+#: templates/orders/print_confirmation.html:24
+#: templates/orders/print_confirmation.html:54
+#: templates/orders/print_receipt.html:12
+msgid "Email Address"
+msgstr ""
+
+#: forms/account.py:75 forms/admin.py:358 forms/admin.py:403
+#: forms/admin.py:431 templates/devices/get_info.html:29
+#: templates/devices/summary.html:20 templates/orders/devices.html:37
+msgid "Username"
+msgstr ""
+
+#: forms/admin.py:43 views/product.py:173
+msgid "Unsupported file encoding"
+msgstr ""
+
+#: forms/admin.py:90
+msgid "Group members"
+msgstr ""
+
+#: forms/admin.py:97 templates/admin/users/form.html:9
+msgid "Permissions"
+msgstr ""
+
+#: forms/admin.py:133
+msgid "A location with that name already exists"
+msgstr ""
+
+#: forms/admin.py:192
+#, python-format
+msgid "\"%s\" cannot be used as a username"
+msgstr ""
+
+#: forms/admin.py:234
+#, python-format
+msgid "Syntax error in template: %s"
+msgstr ""
+
+#: forms/admin.py:241
+msgid "Company Name"
+msgstr ""
+
+#: forms/admin.py:243
+msgid "Company Logo"
+msgstr ""
+
+#: forms/admin.py:245
+msgid "Company-wide logo to use in print templates"
+msgstr ""
+
+#: forms/admin.py:250
+msgid "Terms of Service"
+msgstr ""
+
+#: forms/admin.py:252
+msgid ""
+"These terms will be added to your work confirmations and public check-in "
+"site."
+msgstr ""
+
+#: forms/admin.py:258
+msgid "Autocomplete GSX repairs"
+msgstr ""
+
+#: forms/admin.py:259
+msgid "Complete the GSX repair when closing a Service Order"
+msgstr ""
+
+#: forms/admin.py:264 forms/admin.py:371
+msgid "User Account"
+msgstr ""
+
+#: forms/admin.py:266
+msgid "User account to use for the public check-in service"
+msgstr ""
+
+#: forms/admin.py:270 templates/admin/users/groups.html:12 views/stats.py:276
+msgid "Group"
+msgstr ""
+
+#: forms/admin.py:272
+msgid "Users to choose from in the check-in interface"
+msgstr ""
+
+#: forms/admin.py:276 models/repair.py:54
+msgid "Checklist"
+msgstr ""
+
+#: forms/admin.py:278
+msgid "Checklist to show during check-in"
+msgstr ""
+
+#: forms/admin.py:282 models/queue.py:174
+msgid "Queue"
+msgstr ""
+
+#: forms/admin.py:284
+msgid "Orders created through the check-in interface will go into this queue"
+msgstr ""
+
+#: forms/admin.py:289
+msgid "Show timeline"
+msgstr ""
+
+#: forms/admin.py:290
+msgid "Show status timeline on public repair status page"
+msgstr ""
+
+#: forms/admin.py:295
+msgid "Show password"
+msgstr ""
+
+#: forms/admin.py:296
+msgid "Make checkin device password field readable"
+msgstr ""
+
+#: forms/admin.py:301
+msgid "Show checklist results"
+msgstr ""
+
+#: forms/admin.py:302
+msgid "Show checklist results in order confirmation"
+msgstr ""
+
+#: forms/admin.py:306
+msgid "Currency"
+msgstr ""
+
+#: forms/admin.py:319
+msgid "Default GSX account"
+msgstr ""
+
+#: forms/admin.py:321
+msgid "Use this GSX account before and order is assigned to a queue"
+msgstr ""
+
+#: forms/admin.py:327 models/product.py:107 models/product.py:134
+msgid "Margin %"
+msgstr ""
+
+#: forms/admin.py:328
+msgid "Default margin for new products"
+msgstr ""
+
+#: forms/admin.py:334 models/product.py:88
+msgid "VAT %"
+msgstr ""
+
+#: forms/admin.py:335
+msgid "Default VAT for new products"
+msgstr ""
+
+#: forms/admin.py:341
+msgid "Shipping Cost"
+msgstr ""
+
+#: forms/admin.py:342
+msgid "Default shipping cost for new products"
+msgstr ""
+
+#: forms/admin.py:348
+msgid "Track inventory"
+msgstr ""
+
+#: forms/admin.py:349
+msgid "Unchecking this will disable tracking product amounts in your inventory"
+msgstr ""
+
+#: forms/admin.py:353
+msgid "IMAP server"
+msgstr ""
+
+#: forms/admin.py:368 forms/admin.py:410 forms/admin.py:445
+msgid "Use SSL"
+msgstr ""
+
+#: forms/admin.py:373
+msgid "User account to use when creating notes from messages"
+msgstr ""
+
+#: forms/admin.py:378
+msgid "Default Sender"
+msgstr ""
+
+#: forms/admin.py:380 models/account.py:241
+msgid "User"
+msgstr ""
+
+#: forms/admin.py:381 forms/returns.py:53 models/common.py:507
+#: templates/admin/users/form.html:10 views/stats.py:265
+msgid "Location"
+msgstr ""
+
+#: forms/admin.py:382
+msgid "Custom..."
+msgstr ""
+
+#: forms/admin.py:384
+msgid "Select the default sender address for outgoing emails"
+msgstr ""
+
+#: forms/admin.py:396
+msgid "Default subject"
+msgstr ""
+
+#: forms/admin.py:401
+msgid "SMTP server"
+msgstr ""
+
+#: forms/admin.py:413
+msgid "SMS Gateway"
+msgstr ""
+
+#: forms/admin.py:415
+msgid "Built-in"
+msgstr ""
+
+#: forms/admin.py:423 forms/admin.py:456 forms/checkin.py:100
+msgid "Email address"
+msgstr ""
+
+#: forms/admin.py:426
+msgid "URL"
+msgstr ""
+
+#: forms/admin.py:428
+msgid "SMS Server URL"
+msgstr ""
+
+#: forms/admin.py:441
+msgid "Sender"
+msgstr ""
+
+#: forms/admin.py:451
+msgid "Notify locations"
+msgstr ""
+
+#: forms/admin.py:452
+msgid "Daily reports will be sent to the location's email address"
+msgstr ""
+
+#: forms/admin.py:457
+msgid "Send daily reports to this email address"
+msgstr ""
+
+#: forms/admin.py:465
+msgid "Invalid margin %"
+msgstr ""
+
+#: forms/checkin.py:26 models/note.py:54
+#, python-format
+msgid "%s is not a valid phone number"
+msgstr ""
+
+#: forms/checkin.py:31
+#, python-format
+msgid "%s is not a valid serial or IMEI number"
+msgstr ""
+
+#: forms/checkin.py:40 forms/repairs.py:22 models/customer.py:99
+#: templates/customers/view.html:38
+msgid "Country"
+msgstr ""
+
+#: forms/checkin.py:46 templates/orders/devices.html:54
+#: templates/orders/print_confirmation.html:79
+msgid "Accessories"
+msgstr ""
+
+#: forms/checkin.py:48
+msgid ""
+"Please list here any accessories you'd like to check in with your device "
+"(cables, power adapters, bags, etc)"
+msgstr ""
+
+#: forms/checkin.py:53
+msgid "Proof of Purchase"
+msgstr ""
+
+#: forms/checkin.py:54
+msgid "Proof of Purchase is required when setting purchase date manually"
+msgstr ""
+
+#: forms/checkin.py:93 forms/repairs.py:15
+msgid "First name"
+msgstr ""
+
+#: forms/checkin.py:94 forms/repairs.py:16
+msgid "Last name"
+msgstr ""
+
+#: forms/checkin.py:97
+msgid "Company (optional)"
+msgstr ""
+
+#: forms/checkin.py:104
+msgid "Phone number"
+msgstr ""
+
+#: forms/checkin.py:107 forms/repairs.py:19 models/invoices.py:75
+#: templates/customers/find.html:51 templates/customers/view.html:30
+#: templates/orders/print_confirmation.html:57
+#: templates/orders/print_receipt.html:15
+msgid "Address"
+msgstr ""
+
+#: forms/checkin.py:108 forms/repairs.py:21 templates/customers/find.html:53
+msgid "City"
+msgstr ""
+
+#: forms/checkin.py:109 templates/customers/find.html:52
+msgid "Postal Code"
+msgstr ""
+
+#: forms/checkin.py:112
+msgid "Check-in location"
+msgstr ""
+
+#: forms/checkin.py:115
+msgid "Choose where you want to leave the device"
+msgstr ""
+
+#: forms/checkin.py:119
+msgid "Check-out location"
+msgstr ""
+
+#: forms/checkin.py:122
+msgid "Choose where you want to pick up the device"
+msgstr ""
+
+#: forms/checkin.py:124
+msgid ""
+"I agree to the <a href=\"/checkin/terms/\" target=\"_blank\">terms of "
+"service.</a>"
+msgstr ""
+
+#: forms/checkin.py:139
+msgid "Serial number or IMEI"
+msgstr ""
+
+#: forms/checkin.py:146 templates/notes/view_note.html:21
+#: templates/products/receive_item.html:17
+#: templates/repairs/get_details.html:10
+#: templates/search/results/gsx_repair_details.html:6
+#: templates/shipments/list_incoming.html:18
+msgid "Service Order"
+msgstr ""
+
+#: forms/checkin.py:147
+msgid "Invalid Service Order number"
+msgstr ""
+
+#: forms/checkin.py:155 templates/checkin/issue.html:7
+msgid "Problem description"
+msgstr ""
+
+#: forms/checkin.py:160 templates/orders/repair.html:23
+msgid "Attachment"
+msgstr ""
+
+#: forms/checkin.py:161
+msgid "Please use this to attach relevant documents"
+msgstr ""
+
+#: forms/checkin.py:166 models/common.py:457
+#: templates/accounts/print_calendar.html:45
+#: templates/accounts/view_calendar.html:32 templates/customers/view.html:42
+#: templates/devices/get_info.html:41
+#: templates/devices/search_gsx_warranty.html:48
+#: templates/devices/summary.html:28 templates/notes/list_notes.html:74
+#: templates/orders/devices.html:49 templates/orders/repair.html:32
+#: templates/repairs/get_details.html:16 templates/search/spotlight.html:27
+#: templates/search/results/gsx_repair_details.html:12
+msgid "Notes"
+msgstr ""
+
+#: forms/checkin.py:168
+msgid "Will not appear on the print-out"
+msgstr ""
+
+#: forms/customer.py:35
+msgid "Enter a valid phone number"
+msgstr ""
+
+#: forms/customer.py:44 forms/product.py:17
+msgid "Name contains"
+msgstr ""
+
+#: forms/customer.py:48
+msgid "Email contains"
+msgstr ""
+
+#: forms/customer.py:52
+msgid "Address contains"
+msgstr ""
+
+#: forms/customer.py:56
+msgid "Checked in between"
+msgstr ""
+
+#: forms/customer.py:72
+msgid "Please specify at least one parameter"
+msgstr ""
+
+#: forms/customer.py:76
+msgid "CSV file"
+msgstr ""
+
+#: forms/customer.py:80
+msgid "Skip duplicates"
+msgstr ""
+
+#: forms/customer.py:81
+msgid "Skip customers with existing email addresses"
+msgstr ""
+
+#: forms/customer.py:87
+msgid "Data file should be in text format"
+msgstr ""
+
+#: forms/devices.py:26 forms/orders.py:97
+msgid "Created between"
+msgstr ""
+
+#: forms/devices.py:34
+msgid "Serial number contains"
+msgstr ""
+
+#: forms/devices.py:42 forms/product.py:25
+msgid "Description contains"
+msgstr ""
+
+#: forms/devices.py:61
+msgid "Serial numbers may not include spaces"
+msgstr ""
+
+#: forms/invoices.py:39 forms/orders.py:57 forms/product.py:125
+msgid "State is"
+msgstr ""
+
+#: forms/invoices.py:41 forms/invoices.py:50 forms/product.py:127
+msgid "Any"
+msgstr ""
+
+#: forms/invoices.py:42 forms/product.py:128 models/order.py:128
+msgid "Open"
+msgstr ""
+
+#: forms/invoices.py:43
+msgid "Paid"
+msgstr ""
+
+#: forms/invoices.py:49
+msgid "Payment method is"
+msgstr ""
+
+#: forms/invoices.py:55
+msgid "Status is not"
+msgstr ""
+
+#: forms/invoices.py:61 forms/invoices.py:64 forms/product.py:140
+#: forms/product.py:143 forms/product.py:173 forms/product.py:187
+#: views/stats.py:250
+msgid "Start date"
+msgstr ""
+
+#: forms/invoices.py:69 forms/invoices.py:72 forms/product.py:148
+#: forms/product.py:151 forms/product.py:180 forms/product.py:194
+#: views/stats.py:255
+msgid "End date"
+msgstr ""
+
+#: forms/invoices.py:77
+msgid "Customer name contains"
+msgstr ""
+
+#: forms/invoices.py:81 forms/notes.py:43
+msgid "Service Order is"
+msgstr ""
+
+#: forms/notes.py:29 models/note.py:81 templates/notes/view_note.html:19
+msgid "From"
+msgstr ""
+
+#: forms/notes.py:40
+msgid "Body contains"
+msgstr ""
+
+#: forms/notes.py:41
+msgid "Recipient contains"
+msgstr ""
+
+#: forms/notes.py:42
+msgid "Sender contains"
+msgstr ""
+
+#: forms/orders.py:52 forms/product.py:165
+msgid "Location is"
+msgstr ""
+
+#: forms/orders.py:62
+msgid "Queue is"
+msgstr ""
+
+#: forms/orders.py:67 models/queue.py:236 templates/admin/queues/form.html:27
+#: templates/devices/search_gsx_repairs.html:9
+#: templates/invoices/index.html:47 templates/orders/list.html:14
+#: templates/repairs/get_details.html:20
+#: templates/search/results/gsx_repair_details.html:16 views/stats.py:286
+msgid "Status"
+msgstr ""
+
+#: forms/orders.py:72
+msgid "Created by"
+msgstr ""
+
+#: forms/orders.py:77 templates/orders/list.html:13
+msgid "Assigned to"
+msgstr ""
+
+#: forms/orders.py:82 templates/orders/edit.html:91
+msgid "Label"
+msgstr ""
+
+#: forms/orders.py:87 models/common.py:345
+msgid "Green"
+msgstr ""
+
+#: forms/orders.py:88
+msgid "Yellow"
+msgstr ""
+
+#: forms/orders.py:89 models/common.py:347
+msgid "Red"
+msgstr ""
+
+#: forms/orders.py:90
+msgid "Grey"
+msgstr ""
+
+#: forms/orders.py:92
+msgid "Color"
+msgstr ""
+
+#: forms/product.py:21
+msgid "Code contains"
+msgstr ""
+
+#: forms/product.py:29
+msgid "Device model is"
+msgstr ""
+
+#: forms/product.py:40
+msgid "Product datafile"
+msgstr ""
+
+#: forms/product.py:48
+msgid "Parts database file"
+msgstr ""
+
+#: forms/product.py:52
+msgid "Import vintage parts"
+msgstr ""
+
+#: forms/product.py:57
+msgid "Update product prices"
+msgstr ""
+
+#: forms/product.py:112
+#, python-format
+msgid "Product code %s contains invalid characters"
+msgstr ""
+
+#: forms/product.py:129
+msgid "Submitted"
+msgstr ""
+
+#: forms/product.py:130 templates/products/receive_item.html:26
+#: templates/products/view.html:158 templates/purchases/list_pos.html:56
+msgid "Received"
+msgstr ""
+
+#: forms/product.py:156
+msgid "Reference contains"
+msgstr ""
+
+#: forms/product.py:170
+msgid "Ordered between"
+msgstr ""
+
+#: forms/product.py:184
+msgid "Received between"
+msgstr ""
+
+#: forms/product.py:198
+msgid "Confirmation is"
+msgstr ""
+
+#: forms/product.py:201
+msgid "Service order is"
+msgstr ""
+
+#: forms/repairs.py:17 models/invoices.py:69 templates/customers/find.html:49
+#: templates/customers/view.html:22
+msgid "Email"
+msgstr ""
+
+#: forms/repairs.py:18 models/invoices.py:63
+#: templates/accounts/print_calendar.html:24 templates/customers/find.html:50
+#: templates/customers/search.html:18 templates/customers/view.html:26
+#: templates/orders/print_confirmation.html:25
+#: templates/search/results/customers.html:14
+msgid "Phone"
+msgstr ""
+
+#: forms/repairs.py:20 models/common.py:413 models/customer.py:88
+msgid "ZIP Code"
+msgstr ""
+
+#: forms/repairs.py:23 models/common.py:325 models/product.py:224
+msgid "Other"
+msgstr ""
+
+#: forms/repairs.py:58 models/repair.py:103
+msgid "Technician"
+msgstr ""
+
+#: forms/repairs.py:73
+msgid "Replacement serial number must be set"
+msgstr ""
+
+#: forms/returns.py:16
+msgid "Reason"
+msgstr ""
+
+#: forms/returns.py:18 forms/returns.py:29 forms/returns.py:39
+msgid "Select..."
+msgstr ""
+
+#: forms/returns.py:19
+msgid "Part not needed"
+msgstr ""
+
+#: forms/returns.py:20
+msgid "Duplicated part"
+msgstr ""
+
+#: forms/returns.py:21
+msgid "Added wrong part"
+msgstr ""
+
+#: forms/returns.py:22
+msgid "Tried to cancel order"
+msgstr ""
+
+#: forms/returns.py:23
+msgid "Customer refused order"
+msgstr ""
+
+#: forms/returns.py:27
+msgid "Type"
+msgstr ""
+
+#: forms/returns.py:30
+msgid "Diagnostic"
+msgstr ""
+
+#: forms/returns.py:31
+msgid "Un-Opened"
+msgstr ""
+
+#: forms/returns.py:38 models/parts.py:46
+msgid "Symptom Code"
+msgstr ""
+
+#: forms/returns.py:42 models/parts.py:51
+msgid "Symptom Modifier"
+msgstr ""
+
+#: management/commands/cron.py:81 models/order.py:127
+msgid "Unassigned"
+msgstr ""
+
+#: management/commands/cron.py:82
+msgid "No Status"
+msgstr ""
+
+#: management/commands/cron.py:86
+#, python-format
+msgid "Repairs aging beyond limits at %s"
+msgstr ""
+
+#: management/commands/cron.py:115
+msgid "Products stocked below limit"
+msgstr ""
+
+#: messaging/sms.py:14
+msgid "SMS sender not configured"
+msgstr ""
+
+#: messaging/sms.py:51
+msgid "SMS sender name not configured"
+msgstr ""
+
+#: messaging/sms.py:77
+#, python-format
+msgid "Failed to send message to %s"
+msgstr ""
+
+#: messaging/sms.py:90
+msgid "Wrong sender name"
+msgstr ""
+
+#: messaging/sms.py:91
+msgid "Too many messages in one request"
+msgstr ""
+
+#: messaging/sms.py:92
+msgid "Invalid username or password"
+msgstr ""
+
+#: messaging/sms.py:93
+msgid "Insufficient credits on your account"
+msgstr ""
+
+#: messaging/sms.py:94
+msgid "Unsuccessful message submission"
+msgstr ""
+
+#: messaging/sms.py:95 messaging/sms.py:96
+msgid "Internal system error"
+msgstr ""
+
+#: messaging/sms.py:118
+#, python-format
+msgid "Unknown error (%s)"
+msgstr ""
+
+#: messaging/sms.py:132
+msgid "No SMS HTTP gateway defined"
+msgstr ""
+
+#: models/account.py:53 templates/admin/users/index.html:7
+msgid "New User"
+msgstr ""
+
+#: models/account.py:64 templates/orders/edit.html:33
+msgid "Current Location"
+msgstr ""
+
+#: models/account.py:65
+msgid "Orders you create will be registered to this location."
+msgstr ""
+
+#: models/account.py:71
+msgid "queues"
+msgstr ""
+
+#: models/account.py:74
+msgid "Danish"
+msgstr ""
+
+#: models/account.py:75
+msgid "Dutch"
+msgstr ""
+
+#: models/account.py:76
+msgid "English"
+msgstr ""
+
+#: models/account.py:77
+msgid "Estonian"
+msgstr ""
+
+#: models/account.py:78
+msgid "Finnish"
+msgstr ""
+
+#: models/account.py:79
+msgid "Swedish"
+msgstr ""
+
+#: models/account.py:85
+msgid "language"
+msgstr ""
+
+#: models/account.py:86
+msgid "Select which language you want to use Servo in."
+msgstr ""
+
+#: models/account.py:94 models/common.py:428
+msgid "Time zone"
+msgstr ""
+
+#: models/account.py:98
+msgid "Denmark"
+msgstr ""
+
+#: models/account.py:99
+msgid "Estonia"
+msgstr ""
+
+#: models/account.py:100
+msgid "Finland"
+msgstr ""
+
+#: models/account.py:101
+msgid "United States"
+msgstr ""
+
+#: models/account.py:102
+msgid "Netherlands"
+msgstr ""
+
+#: models/account.py:103
+msgid "Sweden"
+msgstr ""
+
+#: models/account.py:109
+msgid "region"
+msgstr ""
+
+#: models/account.py:110
+msgid "Affects formatting of numbers, dates and currencies."
+msgstr ""
+
+#: models/account.py:114
+msgid "email notifications"
+msgstr ""
+
+#: models/account.py:115
+msgid "Event notifications will also be emailed to you."
+msgstr ""
+
+#: models/account.py:119
+msgid "print automatically"
+msgstr ""
+
+#: models/account.py:120
+msgid "Opens print dialog automatically."
+msgstr ""
+
+#: models/account.py:126
+msgid "tech ID"
+msgstr ""
+
+#: models/account.py:132 models/common.py:190
+msgid "User ID"
+msgstr ""
+
+#: models/account.py:144
+msgid "PO prefix"
+msgstr ""
+
+#: models/account.py:145
+msgid "GSX repairs you create will be prefixed"
+msgstr ""
+
+#: models/account.py:152 models/customer.py:107 models/device.py:152
+#: models/product.py:202
+msgid "photo"
+msgstr ""
+
+#: models/account.py:153
+msgid "Maximum avatar size is 1MB"
+msgstr ""
+
+#: models/account.py:242 templates/admin/index.html:24 views/admin.py:405
+msgid "Users & Groups"
+msgstr ""
+
+#: models/calendar.py:25 models/common.py:632 models/common.py:709
+#: models/order.py:704 models/repair.py:32
+msgid "title"
+msgstr ""
+
+#: models/calendar.py:26 templates/accounts/calendars.html:7
+msgid "New Calendar"
+msgstr ""
+
+#: models/calendar.py:31
+msgid "hours per day"
+msgstr ""
+
+#: models/calendar.py:32
+msgid "How many hours per day should be in this calendar"
+msgstr ""
+
+#: models/calendar.py:49
+#, python-format
+msgid ""
+"%(hours)s hours total in %(workdays)s days (%(overtime)s hours overtime)."
+msgstr ""
+
+#: models/calendar.py:52
+#, python-format
+msgid "%(hours)s hours total in %(workdays)s days."
+msgstr ""
+
+#: models/common.py:176
+msgid "New GSX Account"
+msgstr ""
+
+#: models/common.py:177 models/queue.py:126
+msgid "Sold-To"
+msgstr ""
+
+#: models/common.py:178 models/common.py:444
+msgid "Ship-To"
+msgstr ""
+
+#: models/common.py:183
+msgid "Region"
+msgstr ""
+
+#: models/common.py:202
+msgid "Environment"
+msgstr ""
+
+#: models/common.py:227
+msgid "Default GSX account not configured"
+msgstr ""
+
+#: models/common.py:252
+msgid "Configuration error"
+msgstr ""
+
+#: models/common.py:297 models/escalations.py:25
+msgid "GSX Account"
+msgstr ""
+
+#: models/common.py:298 templates/admin/index.html:12
+msgid "GSX Accounts"
+msgstr ""
+
+#: models/common.py:317 templates/admin/tags/index.html:6
+msgid "New Tag"
+msgstr ""
+
+#: models/common.py:318 models/common.py:393 models/customer.py:25
+#: models/customer.py:59 models/queue.py:197
+msgid "name"
+msgstr ""
+
+#: models/common.py:322 templates/devices/index.html:49
+#: templates/devices/search_gsx.html:8 templates/devices/search_gsx.html:10
+#: templates/devices/search_gsx_error.html:8 templates/devices/view.html:7
+#: templates/search/results/gsx.html:8 templates/search/results/gsx.html:10
+msgid "Device"
+msgstr ""
+
+#: models/common.py:323 models/common.py:625
+#: templates/accounts/updates.html:37 templates/invoices/index.html:44
+#: templates/notes/form.html:109 templates/orders/dispatch.html:105
+#: templates/orders/edit.html:114 templates/orders/gsx_repair_form.html:89
+#: templates/orders/list.html:10 templates/orders/parts.html:33
+#: templates/orders/tabs.html:4 templates/products/index_outgoing.html:25
+#: templates/products/view.html:129 templates/products/view.html.py:155
+#: templates/products/view.html:180 templates/purchases/list_pos.html:57
+#: templates/shipments/list_returns.html:14
+msgid "Order"
+msgstr ""
+
+#: models/common.py:324 templates/notes/form.html:37
+msgid "Note"
+msgstr ""
+
+#: models/common.py:331 models/common.py:639
+msgid "type"
+msgstr ""
+
+#: models/common.py:344
+msgid "Default"
+msgstr ""
+
+#: models/common.py:346
+msgid "Orange"
+msgstr ""
+
+#: models/common.py:348
+msgid "Blue"
+msgstr ""
+
+#: models/common.py:374 templates/orders/notes.html:32
+msgid "Tag"
+msgstr ""
+
+#: models/common.py:375 templates/admin/index.html:30
+#: templates/devices/form.html:25 templates/products/form.html:51
+msgid "Tags"
+msgstr ""
+
+#: models/common.py:394 templates/admin/locations/index.html:5
+msgid "New Location"
+msgstr ""
+
+#: models/common.py:400 models/customer.py:71
+msgid "phone"
+msgstr ""
+
+#: models/common.py:402 models/customer.py:76
+msgid "email"
+msgstr ""
+
+#: models/common.py:407 models/customer.py:82
+msgid "address"
+msgstr ""
+
+#: models/common.py:419 models/customer.py:94
+msgid "city"
+msgstr ""
+
+#: models/common.py:437
+msgid "Accounts"
+msgstr ""
+
+#: models/common.py:450
+msgid "Timezone"
+msgstr ""
+
+#: models/common.py:458
+msgid "Will be shown on print templates"
+msgstr ""
+
+#: models/common.py:465
+msgid "Logo"
+msgstr ""
+
+#: models/common.py:470
+msgid "Enabled"
+msgstr ""
+
+#: models/common.py:508 templates/admin/index.html:15
+#: templates/stats/index.html:9 templates/stats/locations.html:6
+#: templates/stats/queues.html:6 templates/stats/sales.html:6
+#: templates/stats/statuses.html:6 views/admin.py:510
+msgid "Locations"
+msgstr ""
+
+#: models/common.py:624 templates/customers/view.html:7
+#: templates/devices/search_gsx_repairs.html:8
+#: templates/invoices/index.html:45 templates/orders/dispatch.html:20
+#: templates/orders/gsx_repair_form.html:44 templates/orders/list.html:11
+#: templates/products/view.html:130
+msgid "Customer"
+msgstr ""
+
+#: models/common.py:626 templates/orders/dispatch.html:63
+#: templates/orders/print_confirmation.html:105
+#: templates/orders/print_dispatch.html:13 templates/orders/products.html:10
+#: templates/products/list.html:7 templates/products/view.html:43
+#: templates/purchases/edit_po.html:32 templates/purchases/view_po.html:31
+msgid "Product"
+msgstr ""
+
+#: models/common.py:631 templates/admin/fields/index.html:6
+msgid "New Field"
+msgstr ""
+
+#: models/common.py:645
+msgid "format"
+msgstr ""
+
+#: models/common.py:647
+msgid "value"
+msgstr ""
+
+#: models/common.py:664
+msgid "Field"
+msgstr ""
+
+#: models/common.py:665 templates/admin/index.html:27
+msgid "Fields"
+msgstr ""
+
+#: models/common.py:710 templates/admin/templates/list_templates.html:6
+msgid "New Template"
+msgstr ""
+
+#: models/common.py:713
+msgid "content"
+msgstr ""
+
+#: models/common.py:732
+msgid "Template"
+msgstr ""
+
+#: models/common.py:733 templates/admin/index.html:33
+#: templates/admin/queues/form.html:13
+msgid "Templates"
+msgstr ""
+
+#: models/common.py:741
+msgid "file"
+msgstr ""
+
+#: models/common.py:761
+#, python-format
+msgid "%s is not of an allowed file type"
+msgstr ""
+
+#: models/customer.py:24 templates/admin/users/index.html:12
+#: templates/customers/index.html:16
+msgid "New Group"
+msgstr ""
+
+#: models/customer.py:54 models/customer.py:142
+msgid "company"
+msgstr ""
+
+#: models/customer.py:60 templates/customers/choose.html:17
+#: templates/customers/index.html:7 templates/customers/search.html:5
+#: templates/search/spotlight.html:6
+msgid "New Customer"
+msgstr ""
+
+#: models/customer.py:114 templates/admin/users/tabs.html:6
+#: templates/customers/index.html:55
+msgid "Groups"
+msgstr ""
+
+#: models/customer.py:121
+msgid "tags"
+msgstr ""
+
+#: models/customer.py:128 models/device.py:146
+msgid "notes"
+msgstr ""
+
+#: models/customer.py:136
+msgid "devices"
+msgstr ""
+
+#: models/device.py:39 templates/devices/find.html:27
+#: templates/devices/get_info.html:14
+#: templates/devices/search_gsx_warranty.html:15
+#: templates/devices/summary.html:13 templates/orders/devices.html:20
+#: templates/orders/print_confirmation.html:72
+#: templates/orders/products.html:28 templates/products/index_outgoing.html:28
+#: templates/search/results/gsx_warranty.html:15
+#: templates/shipments/list_returns.html:17
+msgid "Serial Number"
+msgstr ""
+
+#: models/device.py:43 templates/devices/choose.html:17
+#: templates/devices/index.html:7 templates/devices/index.html.py:10
+msgid "New Device"
+msgstr ""
+
+#: models/device.py:44 models/order.py:708 models/queue.py:44
+#: models/queue.py:202
+msgid "description"
+msgstr ""
+
+#: models/device.py:49
+msgid "Apple"
+msgstr ""
+
+#: models/device.py:50 models/product.py:183
+msgid "Brand"
+msgstr ""
+
+#: models/device.py:57 templates/orders/print_confirmation.html:68
+msgid "IMEI Number"
+msgstr ""
+
+#: models/device.py:63 templates/devices/search_gsx_warranty.html:24
+#: templates/orders/devices.html:31
+msgid "Initial Activation Policy"
+msgstr ""
+
+#: models/device.py:69 templates/devices/search_gsx_warranty.html:22
+#: templates/orders/devices.html:29
+msgid "Applied Activation Policy"
+msgstr ""
+
+#: models/device.py:79 templates/devices/index.html:26
+msgid "Product Line"
+msgstr ""
+
+#: models/device.py:85
+msgid "Products that are compatible with this device instance"
+msgstr ""
+
+#: models/device.py:92
+msgid "configuration"
+msgstr ""
+
+#: models/device.py:96 models/parts.py:78
+msgid "Apple Limited Warranty"
+msgstr ""
+
+#: models/device.py:97 models/parts.py:82
+msgid "AppleCare Protection Plan"
+msgstr ""
+
+#: models/device.py:98
+msgid "Customer Satisfaction (CS) Code"
+msgstr ""
+
+#: models/device.py:99 models/parts.py:75
+msgid "Custom Bid Contracts"
+msgstr ""
+
+#: models/device.py:100
+msgid "3'rd Party Warranty"
+msgstr ""
+
+#: models/device.py:101
+msgid "Out Of Warranty (No Coverage)"
+msgstr ""
+
+#: models/device.py:102 views/gsx.py:131
+msgid "Unknown"
+msgstr ""
+
+#: models/device.py:109 templates/checkin/device.html:16
+#: templates/devices/find.html:30 templates/devices/get_info.html:10
+#: templates/devices/search_gsx_warranty.html:11
+#: templates/devices/summary.html:9 templates/orders/devices.html:16
+#: templates/orders/print_confirmation.html:65
+#: templates/search/results/gsx_warranty.html:11
+msgid "Warranty Status"
+msgstr ""
+
+#: models/device.py:115
+msgid "username"
+msgstr ""
+
+#: models/device.py:121
+msgid "password"
+msgstr ""
+
+#: models/device.py:126
+msgid "Date Purchased"
+msgstr ""
+
+#: models/device.py:134
+msgid "Purchase Country"
+msgstr ""
+
+#: models/device.py:158
+msgid "Image URL"
+msgstr ""
+
+#: models/device.py:163
+msgid "Manual URL"
+msgstr ""
+
+#: models/device.py:168 templates/devices/view.html:38
+#: templates/devices/view.html.py:40
+msgid "Exploded View"
+msgstr ""
+
+#: models/device.py:285
+#, python-format
+msgid "Invalid argument for warranty check: %s"
+msgstr ""
+
+#: models/device.py:352
+#, python-format
+msgid "SLA Group: %s"
+msgstr ""
+
+#: models/device.py:354
+msgid "This unit is eligible for Onsite Service."
+msgstr ""
+
+#: models/device.py:356
+msgid "Parts and Labor are covered."
+msgstr ""
+
+#: models/escalations.py:96
+#, python-format
+msgid "Escalation %s"
+msgstr ""
+
+#: models/invoices.py:27 models/invoices.py:163
+msgid "No Charge"
+msgstr ""
+
+#: models/invoices.py:28 models/invoices.py:164
+msgid "Cash"
+msgstr ""
+
+#: models/invoices.py:29 models/invoices.py:165
+#: templates/products/view.html:179
+msgid "Invoice"
+msgstr ""
+
+#: models/invoices.py:30 models/invoices.py:166
+msgid "Credit Card"
+msgstr ""
+
+#: models/invoices.py:31 models/invoices.py:167
+msgid "Mail payment"
+msgstr ""
+
+#: models/invoices.py:32 models/invoices.py:168
+msgid "Online payment"
+msgstr ""
+
+#: models/invoices.py:39 models/invoices.py:173
+#: templates/invoices/index.html:50
+msgid "Payment Method"
+msgstr ""
+
+#: models/invoices.py:42
+msgid "paid"
+msgstr ""
+
+#: models/invoices.py:56
+msgid "Walk-in"
+msgstr ""
+
+#: models/invoices.py:57 templates/customers/find.html:48
+#: templates/orders/print_confirmation.html:177
+msgid "Name"
+msgstr ""
+
+#: models/invoices.py:81 models/repair.py:127
+#: templates/devices/search_gsx_repairs.html:6 templates/orders/repair.html:30
+#: templates/products/receive_item.html:23
+#: templates/products/receive_item.html:30 templates/purchases/edit_po.html:33
+#: templates/purchases/list_pos.html:54 templates/purchases/view_po.html:21
+#: templates/purchases/view_po.html:32
+#: templates/shipments/edit_bulk_return.html:57
+msgid "Reference"
+msgstr ""
+
+#: models/invoices.py:132
+msgid "Sales Price"
+msgstr ""
+
+#: models/invoices.py:186 views/invoices.py:159
+#, python-format
+msgid "Order %s dispatched"
+msgstr ""
+
+#: models/invoices.py:195
+#, python-format
+msgid "Payment for %0.2f received"
+msgstr ""
+
+#: models/note.py:66
+msgid "subject"
+msgstr ""
+
+#: models/note.py:70
+msgid "Message"
+msgstr ""
+
+#: models/note.py:87
+msgid "To"
+msgstr ""
+
+#: models/note.py:112 models/order.py:763
+msgid "report"
+msgstr ""
+
+#: models/note.py:116
+msgid "read"
+msgstr ""
+
+#: models/note.py:121
+msgid "flagged"
+msgstr ""
+
+#: models/note.py:159
+#, python-format
+msgid "Default Address <%s>"
+msgstr ""
+
+#: models/note.py:271
+msgid "As Unreported"
+msgstr ""
+
+#: models/note.py:271
+msgid "As Reported"
+msgstr ""
+
+#: models/note.py:274
+msgid "As Unread"
+msgstr ""
+
+#: models/note.py:274
+msgid "As Read"
+msgstr ""
+
+#: models/note.py:277
+msgid "As Unflagged"
+msgstr ""
+
+#: models/note.py:277
+msgid "As Flagged"
+msgstr ""
+
+#: models/note.py:319 models/note.py:409
+#, python-format
+msgid "Already sent message to %s"
+msgstr ""
+
+#: models/note.py:364 models/note.py:448
+#, python-format
+msgid "Message sent to %s"
+msgstr ""
+
+#: models/note.py:385
+msgid "System is not configured for built-in SMS support."
+msgstr ""
+
+#: models/note.py:417
+msgid "SMS gateway not configured"
+msgstr ""
+
+#: models/note.py:473
+#, python-format
+msgid "Escalation %s created"
+msgstr ""
+
+#: models/note.py:476
+#, python-format
+msgid "Escalation %s updated"
+msgstr ""
+
+#: models/note.py:481
+msgid "Note saved"
+msgstr ""
+
+#: models/order.py:41 models/queue.py:60
+msgid "priority"
+msgstr ""
+
+#: models/order.py:106 models/repair.py:39
+msgid "queue"
+msgstr ""
+
+#: models/order.py:112
+msgid "status"
+msgstr ""
+
+#: models/order.py:129
+msgid "Closed"
+msgstr ""
+
+#: models/order.py:215
+#, python-format
+msgid "%s added"
+msgstr ""
+
+#: models/order.py:219
+msgid "This device has already been added to this order"
+msgstr ""
+
+#: models/order.py:237
+#, python-format
+msgid "%s removed"
+msgstr ""
+
+#: models/order.py:260
+msgid "Closed a moment ago"
+msgstr ""
+
+#: models/order.py:261
+#, python-format
+msgid "Closed for %(time)s"
+msgstr ""
+
+#: models/order.py:265
+#, python-format
+msgid "%s a moment ago"
+msgstr ""
+
+#: models/order.py:268
+#, python-format
+msgid "%(status)s for %(time)s"
+msgstr ""
+
+#: models/order.py:272
+msgid "Created a moment ago"
+msgstr ""
+
+#: models/order.py:273
+#, python-format
+msgid "Unassigned for %(delta)s"
+msgstr ""
+
+#: models/order.py:277
+msgid "Started a moment ago"
+msgstr ""
+
+#: models/order.py:278
+#, python-format
+msgid "Open for %(delta)s"
+msgstr ""
+
+#: models/order.py:281
+msgid "Select place"
+msgstr ""
+
+#: models/order.py:284
+msgid "Select status"
+msgstr ""
+
+#: models/order.py:291
+msgid "Select user"
+msgstr ""
+
+#: models/order.py:294
+msgid "Select queue"
+msgstr ""
+
+#: models/order.py:303 templates/default.html:35
+#: templates/accounts/orders.html:58 templates/accounts/tabs.html:7
+#: templates/customers/view.html:8 templates/devices/view.html:8
+#: templates/orders/index.html:49 templates/search/spotlight.html:22
+#: views/order.py:43
+msgid "Orders"
+msgstr ""
+
+#: models/order.py:312 views/order.py:195
+#, python-format
+msgid "Order %s closed"
+msgstr ""
+
+#: models/order.py:411
+msgid "Order must belong to a queue to set status"
+msgstr ""
+
+#: models/order.py:445
+msgid "Status unassigned"
+msgstr ""
+
+#: models/order.py:475
+msgid "Removed from queue"
+msgstr ""
+
+#: models/order.py:501
+msgid "Order unassigned"
+msgstr ""
+
+#: models/order.py:505
+#, python-format
+msgid "Order %(order)s assigned to %(user)s"
+msgstr ""
+
+#: models/order.py:598 views/order.py:693 views/purchases.py:90
+#, python-format
+msgid "Product %s added"
+msgstr ""
+
+#: models/order.py:604
+#, python-format
+msgid "Product %s removed from order"
+msgstr ""
+
+#: models/order.py:685
+msgid "Can set assignee"
+msgstr ""
+
+#: models/order.py:686
+msgid "Can change status"
+msgstr ""
+
+#: models/order.py:687
+msgid "Can follow order"
+msgstr ""
+
+#: models/order.py:719
+msgid "amount"
+msgstr ""
+
+#: models/order.py:724
+msgid "KGB Serial Number"
+msgstr ""
+
+#: models/order.py:759
+msgid "dispatched"
+msgstr ""
+
+#: models/order.py:768
+msgid "sales price"
+msgstr ""
+
+#: models/order.py:783
+msgid "KBB Serial Number"
+msgstr ""
+
+#: models/order.py:790 templates/orders/print_confirmation.html:120
+#: templates/orders/print_receipt.html:23
+msgid "IMEI"
+msgstr ""
+
+#: models/order.py:794 templates/orders/print_confirmation.html:106
+#: templates/orders/print_dispatch.html:14
+msgid "Warranty"
+msgstr ""
+
+#: models/order.py:795 templates/devices/parts.html:11
+#: templates/products/form.html:11 templates/products/get_info.html:23
+#: templates/products/list.html:9 templates/products/view.html:64
+msgid "Exchange Price"
+msgstr ""
+
+#: models/order.py:796 templates/devices/parts.html:10
+#: templates/products/form.html:12 templates/products/get_info.html:21
+#: templates/products/list.html:8 templates/products/view.html:62
+msgid "Stock Price"
+msgstr ""
+
+#: models/order.py:803
+msgid "Price category"
+msgstr ""
+
+#: models/order.py:810
+msgid "symptom code"
+msgstr ""
+
+#: models/order.py:816
+msgid "symptom modifier"
+msgstr ""
+
+#: models/order.py:1005 models/product.py:589
+msgid "New Shipping Method"
+msgstr ""
+
+#: models/order.py:1034 views/order.py:238
+#, python-format
+msgid "Order %s created"
+msgstr ""
+
+#: models/parts.py:76
+msgid "Customer Satisfaction"
+msgstr ""
+
+#: models/parts.py:77
+msgid "DOA Coverage"
+msgstr ""
+
+#: models/parts.py:79
+msgid "Missing Upon First Use"
+msgstr ""
+
+#: models/parts.py:80
+msgid "Out of Warranty (no coverage)"
+msgstr ""
+
+#: models/parts.py:81
+msgid "AppleCare Parts Agreement"
+msgstr ""
+
+#: models/parts.py:83
+msgid "Quality Program"
+msgstr ""
+
+#: models/parts.py:84
+msgid "AppleCare Repair Agreement"
+msgstr ""
+
+#: models/parts.py:85 templates/orders/devices.html:13
+msgid "Repeat Service"
+msgstr ""
+
+#: models/parts.py:86
+msgid "Additional Part Coverage"
+msgstr ""
+
+#: models/parts.py:87
+msgid "Additional Service Coverage"
+msgstr ""
+
+#: models/parts.py:88
+msgid "NEW - AppleCare Protection Plan"
+msgstr ""
+
+#: models/parts.py:89
+msgid "Consumer Law Coverage"
+msgstr ""
+
+#: models/parts.py:231
+msgid "Unregister from Return"
+msgstr ""
+
+#: models/parts.py:233 templates/repairs/part_menu.html:8
+msgid "Register for Return"
+msgstr ""
+
+#: models/parts.py:328
+msgid "GSX repair has no dispatch ID"
+msgstr ""
+
+#: models/product.py:64
+msgid "code"
+msgstr ""
+
+#: models/product.py:71
+msgid "Substituted (new) code of this part"
+msgstr ""
+
+#: models/product.py:76 templates/products/index.html:9
+msgid "New Product"
+msgstr ""
+
+#: models/product.py:77 models/queue.py:27
+#: templates/products/index_outgoing.html:24
+#: templates/shipments/list_returns.html:13
+msgid "Title"
+msgstr ""
+
+#: models/product.py:82 models/repair.py:64 templates/devices/find.html:28
+msgid "Description"
+msgstr ""
+
+#: models/product.py:93
+msgid "Don't update price when recalculating prices or importing parts"
+msgstr ""
+
+#: models/product.py:100 models/product.py:128
+msgid "Purchase price"
+msgstr ""
+
+#: models/product.py:113 models/product.py:140
+msgid "Net price"
+msgstr ""
+
+#: models/product.py:114 models/product.py:141
+msgid "Purchase price + margin %"
+msgstr ""
+
+#: models/product.py:120 models/product.py:147
+msgid "Sales price"
+msgstr ""
+
+#: models/product.py:121 models/product.py:148
+msgid "Purchase price + margin % + shipping + VAT %"
+msgstr ""
+
+#: models/product.py:153
+msgid "is serialized"
+msgstr ""
+
+#: models/product.py:154
+msgid "Product has a serial number"
+msgstr ""
+
+#: models/product.py:171
+msgid "Warranty (months)"
+msgstr ""
+
+#: models/product.py:177
+msgid "Shelf code"
+msgstr ""
+
+#: models/product.py:189 templates/admin/inventory/index.html:9
+#: templates/products/index.html:60 templates/products/view.html:10
+msgid "Categories"
+msgstr ""
+
+#: models/product.py:195
+msgid "device models"
+msgstr ""
+
+#: models/product.py:205
+msgid "shipping"
+msgstr ""
+
+#: models/product.py:213
+msgid "component group"
+msgstr ""
+
+#: models/product.py:220
+msgid "Module"
+msgstr ""
+
+#: models/product.py:221
+msgid "Replacement"
+msgstr ""
+
+#: models/product.py:222
+msgid "Service"
+msgstr ""
+
+#: models/product.py:223
+msgid "Service Contract"
+msgstr ""
+
+#: models/product.py:231
+msgid "part type"
+msgstr ""
+
+#: models/product.py:238
+msgid "EEE code"
+msgstr ""
+
+#: models/product.py:252
+#, python-format
+msgid "%d months"
+msgstr ""
+
+#: models/product.py:419
+#, python-format
+msgid "Product %s not found in inventory."
+msgstr ""
+
+#: models/product.py:495
+msgid "Can change product amount"
+msgstr ""
+
+#: models/product.py:507 templates/products/index.html:12
+msgid "New Category"
+msgstr ""
+
+#: models/product.py:557
+msgid "minimum amount"
+msgstr ""
+
+#: models/product.py:561
+msgid "reserved amount"
+msgstr ""
+
+#: models/product.py:565
+msgid "stocked amount"
+msgstr ""
+
+#: models/product.py:569
+msgid "ordered amount"
+msgstr ""
+
+#: models/purchases.py:35
+msgid "The location from which this PO was created"
+msgstr ""
+
+#: models/purchases.py:42
+msgid "reference"
+msgstr ""
+
+#: models/purchases.py:48
+msgid "confirmation"
+msgstr ""
+
+#: models/purchases.py:58
+msgid "supplier"
+msgstr ""
+
+#: models/purchases.py:63 models/shipments.py:65
+msgid "carrier"
+msgstr ""
+
+#: models/purchases.py:68
+msgid "tracking ID"
+msgstr ""
+
+#: models/purchases.py:73
+msgid "delivery Time"
+msgstr ""
+
+#: models/purchases.py:125
+#, python-format
+msgid "Purchase Order %d has already been submitted"
+msgstr ""
+
+#: models/purchases.py:178
+msgid "Submitted orders cannot be deleted"
+msgstr ""
+
+#: models/purchases.py:191 templates/products/receive_item.html:36
+#: templates/purchases/edit_po.html:35 templates/purchases/view_po.html:34
+msgid "Purchase Price"
+msgstr ""
+
+#: models/purchases.py:192
+msgid "Purchase price without taxes"
+msgstr ""
+
+#: models/purchases.py:198
+msgid "Purchase Order"
+msgstr ""
+
+#: models/purchases.py:210
+msgid "arrived"
+msgstr ""
+
+#: models/purchases.py:248
+msgid "Product has already been received"
+msgstr ""
+
+#: models/purchases.py:291
+#, python-format
+msgid "Cannot receive item %(prod)s (%(ref)s)"
+msgstr ""
+
+#: models/purchases.py:318 views/purchases.py:215
+#, python-format
+msgid "Purchase Order %d created"
+msgstr ""
+
+#: models/queue.py:26 templates/admin/queues/index.html:5
+msgid "New Queue"
+msgstr ""
+
+#: models/queue.py:33
+msgid ""
+"Orders with devices matching these keywords will be automatically assigned "
+"to this queue"
+msgstr ""
+
+#: models/queue.py:38
+msgid "locations"
+msgstr ""
+
+#: models/queue.py:39
+msgid "Pick the locations you want this queue to appear in."
+msgstr ""
+
+#: models/queue.py:52
+msgid "High"
+msgstr ""
+
+#: models/queue.py:53
+msgid "Normal"
+msgstr ""
+
+#: models/queue.py:54
+msgid "Low"
+msgstr ""
+
+#: models/queue.py:68
+msgid "Order Created"
+msgstr ""
+
+#: models/queue.py:69
+msgid "Order has ben placed to a queue"
+msgstr ""
+
+#: models/queue.py:77
+msgid "Order Assigned"
+msgstr ""
+
+#: models/queue.py:78
+msgid "Order has ben assigned to a user"
+msgstr ""
+
+#: models/queue.py:86
+msgid "Products Ordered"
+msgstr ""
+
+#: models/queue.py:87
+msgid "Purchase Order for this Service Order has been submitted"
+msgstr ""
+
+#: models/queue.py:94
+msgid "Products Received"
+msgstr ""
+
+#: models/queue.py:95
+msgid "Products have been received"
+msgstr ""
+
+#: models/queue.py:102
+msgid "Repair Completed"
+msgstr ""
+
+#: models/queue.py:103
+msgid "GSX repair completed"
+msgstr ""
+
+#: models/queue.py:111
+msgid "Order Dispatched"
+msgstr ""
+
+#: models/queue.py:119
+msgid "Order Closed"
+msgstr ""
+
+#: models/queue.py:127
+msgid "GSX queries of an order in this queue will be made using this Sold-To"
+msgstr ""
+
+#: models/queue.py:134
+msgid "order template"
+msgstr ""
+
+#: models/queue.py:135
+msgid "HTML template for Service Order/Work Confirmation"
+msgstr ""
+
+#: models/queue.py:141
+msgid "quote template"
+msgstr ""
+
+#: models/queue.py:142
+msgid "HTML template for cost estimate"
+msgstr ""
+
+#: models/queue.py:148
+msgid "receipt template"
+msgstr ""
+
+#: models/queue.py:149
+msgid "HTML template for Sales Order Receipt"
+msgstr ""
+
+#: models/queue.py:155
+msgid "dispatch template"
+msgstr ""
+
+#: models/queue.py:156
+msgid "HTML template for dispatched order"
+msgstr ""
+
+#: models/queue.py:175 templates/admin/index.html:21
+#: templates/stats/index.html:10 templates/stats/locations.html:7
+#: templates/stats/queues.html:7 templates/stats/sales.html:7
+#: templates/stats/statuses.html:7 views/admin.py:577
+msgid "Queues"
+msgstr ""
+
+#: models/queue.py:187
+msgid "Minutes"
+msgstr ""
+
+#: models/queue.py:188
+msgid "Hours"
+msgstr ""
+
+#: models/queue.py:189
+msgid "Days"
+msgstr ""
+
+#: models/queue.py:190
+msgid "Weeks"
+msgstr ""
+
+#: models/queue.py:191
+msgid "Months"
+msgstr ""
+
+#: models/queue.py:196 templates/admin/statuses/index.html:5
+msgid "New Status"
+msgstr ""
+
+#: models/queue.py:206 models/queue.py:249
+msgid "green limit"
+msgstr ""
+
+#: models/queue.py:210 models/queue.py:250
+msgid "yellow limit"
+msgstr ""
+
+#: models/queue.py:215 models/queue.py:253
+msgid "time unit"
+msgstr ""
+
+#: models/queue.py:237 templates/accounts/updates.html:28
+#: templates/admin/index.html:18 templates/admin/queues/form.html:10
+#: templates/stats/index.html:11 templates/stats/locations.html:8
+#: templates/stats/queues.html:8 templates/stats/sales.html:8
+#: templates/stats/statuses.html:8 views/admin.py:261
+msgid "Statuses"
+msgstr ""
+
+#: models/repair.py:33 templates/admin/checklist/index.html:5
+msgid "New Checklist"
+msgstr ""
+
+#: models/repair.py:42
+msgid "enabled"
+msgstr ""
+
+#: models/repair.py:55 templates/admin/index.html:36
+msgid "Checklists"
+msgstr ""
+
+#: models/repair.py:60
+msgid "Task"
+msgstr ""
+
+#: models/repair.py:107
+msgid "Unit Received"
+msgstr ""
+
+#: models/repair.py:120
+msgid "Repair should be reviewed by Apple before confirmation"
+msgstr ""
+
+#: models/repair.py:135
+msgid "Notes are mandatory when requesting review."
+msgstr ""
+
+#: models/repair.py:142
+msgid "Choose files to be sent with the repair creation request"
+msgstr ""
+
+#: models/repair.py:148
+msgid "mark complete"
+msgstr ""
+
+#: models/repair.py:149
+msgid "Requires replacement serial number"
+msgstr ""
+
+#: models/repair.py:155
+msgid "New serial number"
+msgstr ""
+
+#: models/repair.py:156
+msgid "Serial Number of replacement part"
+msgstr ""
+
+#: models/repair.py:175
+msgid "Unit is eligible for consumer law coverage"
+msgstr ""
+
+#: models/repair.py:223
+msgid "New GSX Repair"
+msgstr ""
+
+#: models/repair.py:278
+msgid "Please add some parts to the repair"
+msgstr ""
+
+#: models/repair.py:281
+msgid "Order has not been assigned to a queue"
+msgstr ""
+
+#: models/repair.py:327 views/gsx.py:193
+#, python-format
+msgid "GSX repair %s created"
+msgstr ""
+
+#: models/repair.py:582
+#, python-format
+msgid "Repair %d"
+msgstr ""
+
+#: models/shipments.py:43 templates/shipments/list_bulk_returns.html:10
+msgid "Tracking ID"
+msgstr ""
+
+#: models/shipments.py:90
+msgid "width"
+msgstr ""
+
+#: models/shipments.py:96
+msgid "height"
+msgstr ""
+
+#: models/shipments.py:102
+msgid "length"
+msgstr ""
+
+#: models/shipments.py:108
+msgid "weight"
+msgstr ""
+
+#: templates/default.html:34 templates/default.html.py:66
+msgid "Homepage"
+msgstr ""
+
+#: templates/default.html:36
+msgid "Inventory"
+msgstr ""
+
+#: templates/default.html:37 templates/customers/view.html:9
+#: templates/devices/index.html:77 templates/devices/search.html:14
+#: templates/search/spotlight.html:16 views/device.py:48
+msgid "Devices"
+msgstr ""
+
+#: templates/default.html:38 templates/customers/index.html:82
+#: templates/customers/list.html:5 templates/customers/search.html:33
+#: templates/customers/view.html:117 templates/search/spotlight.html:13
+#: views/customer.py:27
+msgid "Customers"
+msgstr ""
+
+#: templates/default.html:39 templates/accounts/updates.html:27
+#: templates/customers/view.html:10 templates/notes/list_notes.html:36
+#: views/note.py:54
+msgid "Messages"
+msgstr ""
+
+#: templates/default.html:40 templates/accounts/tabs.html:16
+#: templates/stats/index.html:58 views/stats.py:296
+msgid "Statistics"
+msgstr ""
+
+#: templates/default.html:44 templates/accounts/orders.html:44
+#: templates/accounts/stats.html:19 templates/customers/choose.html:18
+#: templates/customers/find.html:16 templates/customers/find.html.py:31
+#: templates/customers/index.html:44 templates/devices/choose.html:18
+#: templates/devices/find.html:8 templates/devices/find.html.py:21
+#: templates/devices/index.html:20 templates/invoices/index.html:31
+#: templates/notes/find.html:9 templates/notes/find.html.py:25
+#: templates/notes/list_notes.html:29 templates/orders/index.html:36
+#: templates/products/choose.html:18 templates/products/index.html:84
+#: templates/purchases/list_pos.html:37
+#: templates/snippets/filtering_form.html:12 templates/stats/index.html:30
+msgid "Search"
+msgstr ""
+
+#: templates/default.html:67 templates/accounts/calendars.html:36
+#: templates/accounts/calendars.html:54 templates/accounts/tabs.html:13
+#: templates/accounts/view_calendar.html:64 views/account.py:128
+msgid "Calendars"
+msgstr ""
+
+#: templates/default.html:68 templates/accounts/settings.html:11
+#: templates/admin/users/form.html:11
+msgid "Profile"
+msgstr ""
+
+#: templates/default.html:70 templates/admin/index.html:7 views/admin.py:205
+msgid "System Settings"
+msgstr ""
+
+#: templates/default.html:73 templates/accounts/logout.html:15
+#: templates/checkin/index.html:47
+msgid "Log out"
+msgstr ""
+
+#: templates/default.html:100
+msgid "No new alerts"
+msgstr ""
+
+#: templates/default.html:105
+msgid "See All Alerts"
+msgstr ""
+
+#: templates/default.html:133
+msgid "No new messages"
+msgstr ""
+
+#: templates/default.html:139
+msgid "Read All Messages"
+msgstr ""
+
+#: templates/default.html:168
+msgid "Terms"
+msgstr ""
+
+#: templates/error.html:12
+msgid "An Error Occurred"
+msgstr ""
+
+#: templates/error.html:27 templates/checkin/error.html:9
+msgid "Oops!"
+msgstr ""
+
+#: templates/error.html:28 templates/checkin/error.html:10
+msgid "It appears that an error has occurred."
+msgstr ""
+
+#: templates/error.html:28
+msgid ""
+"I've notified the developers, but I'm sure they would really appreciate if "
+"you could briefly describe what you were doing before this happened."
+msgstr ""
+
+#: templates/error.html:33 templates/orders/dispatch.html:96
+#: templates/products/form.html:74 templates/products/form.html.py:76
+#: templates/products/receive_item.html:47
+msgid "Cancel"
+msgstr ""
+
+#: templates/error.html:34 templates/accounts/register.html:11
+#: templates/checkin/confirmation.html:11 templates/checkin/customer.html:62
+#: templates/notes/edit_escalation.html:13
+#: templates/orders/gsx_repair_form.html:79
+#: templates/products/index_outgoing.html:49
+#: templates/purchases/edit_po.html:87 templates/purchases/order_stock.html:15
+#: templates/shipments/edit_bulk_return.html:96
+#: templates/shipments/list_returns.html:38
+#: templates/shipments/submit_bulk_return.html:13
+msgid "Submit"
+msgstr ""
+
+#: templates/error.html:38
+msgid "Thanks!"
+msgstr ""
+
+#: templates/error.html:39
+msgid ""
+"Your error report has been submitted. Thanks for helping make Servo better!"
+msgstr ""
+
+#: templates/error.html:40
+msgid "Return Home"
+msgstr ""
+
+#: templates/form_buttons.html:3 templates/accounts/register.html:10
+#: templates/admin/users/form.html:50 templates/checkin/choose_model.html:137
+#: templates/checkin/customer.html:61 templates/checkin/device.html:56
+#: templates/checkin/issue.html:19 templates/checkin/sn.html:14
+#: templates/checkin/status.html:14
+msgid "Back"
+msgstr ""
+
+#: templates/form_buttons.html:4 templates/admin/settings.html:110
+#: templates/admin/checklist/form.html:28 templates/admin/fields/form.html:14
+#: templates/admin/gsx/form.html:17 templates/admin/locations/form.html:37
+#: templates/admin/notifications/index.html:36
+#: templates/admin/statuses/form.html:18 templates/admin/tags/form.html:14
+#: templates/admin/templates/form.html:18 templates/admin/users/form.html:51
+#: templates/notes/form.html:101 templates/orders/gsx_repair_form.html:79
+#: templates/products/form.html:78 templates/purchases/edit_po.html:87
+#: templates/shipments/edit_bulk_return.html:96
+msgid "Save"
+msgstr ""
+
+#: templates/modal.html:10 templates/admin/fields/remove.html:7
+#: templates/admin/statuses/remove.html:9 templates/admin/users/remove.html:9
+msgid "This action cannot be undone."
+msgstr ""
+
+#: templates/modal.html:15 templates/orders/parts.html:20
+#: templates/products/get_info.html:58 templates/snippets/modal.html:12
+msgid "Done"
+msgstr ""
+
+#: templates/pagination.html:11
+msgid "Page"
+msgstr ""
+
+#: templates/accounts/calendars.html:9
+msgid "New Event"
+msgstr ""
+
+#: templates/accounts/calendars.html:15 templates/checkin/thanks.html:13
+#: templates/orders/toolbar.html:20
+msgid "Print"
+msgstr ""
+
+#: templates/accounts/calendars.html:16 templates/customers/find.html:75
+#: templates/customers/find.html.py:77 templates/customers/view.html:85
+#: templates/customers/view.html.py:87 templates/devices/find.html:54
+#: templates/devices/list.html:27 templates/devices/view.html:60
+#: templates/generic/index.html:15 templates/orders/customer.html:20
+#: templates/orders/devices.html:119 templates/orders/products.html:67
+#: templates/products/get_info.html:56 templates/products/view.html:119
+#: templates/products/view.html.py:121 templates/purchases/list_pos.html:92
+#: templates/purchases/list_pos.html:94
+msgid "Edit"
+msgstr ""
+
+#: templates/accounts/calendars.html:18 templates/accounts/calendars.html:20
+#: templates/accounts/delete_calendar.html:15
+#: templates/accounts/delete_calendar_event.html:11
+#: templates/admin/checklist/form.html:24
+#: templates/admin/checklist/form.html:26 templates/admin/fields/form.html:10
+#: templates/admin/fields/form.html:12 templates/admin/gsx/form.html:13
+#: templates/admin/gsx/form.html.py:15 templates/admin/gsx/remove.html:11
+#: templates/admin/locations/form.html:33
+#: templates/admin/locations/form.html:35 templates/admin/queues/form.html:29
+#: templates/admin/queues/remove.html:15 templates/admin/statuses/form.html:14
+#: templates/admin/statuses/form.html:16
+#: templates/admin/statuses/remove.html:15 templates/admin/tags/form.html:10
+#: templates/admin/tags/form.html.py:12 templates/admin/templates/form.html:14
+#: templates/admin/templates/form.html:16
+#: templates/admin/users/delete_group.html:9
+#: templates/admin/users/form.html:46 templates/admin/users/form.html.py:48
+#: templates/admin/users/remove.html:15
+#: templates/customers/delete_group.html:15 templates/customers/find.html:81
+#: templates/customers/find.html.py:83 templates/customers/remove.html:5
+#: templates/customers/remove.html.py:15 templates/customers/view.html:77
+#: templates/customers/view.html.py:79 templates/devices/find.html:56
+#: templates/devices/list.html:29 templates/devices/remove.html:15
+#: templates/devices/view.html:52 templates/devices/view.html.py:54
+#: templates/generic/delete.html:15 templates/generic/index.html:16
+#: templates/notes/form.html:99 templates/notes/remove.html:15
+#: templates/notes/view_note.html:34 templates/orders/delete_order.html:15
+#: templates/orders/gsx_repair_form.html:77 templates/orders/notes.html:50
+#: templates/orders/products.html:77 templates/orders/remove_product.html:15
+#: templates/products/delete_category.html:15
+#: templates/products/list_rows.html:44 templates/products/list_rows.html:46
+#: templates/products/remove.html:11 templates/products/view.html:109
+#: templates/products/view.html.py:113 templates/purchases/edit_po.html:37
+#: templates/purchases/list_pos.html:97 templates/purchases/list_pos.html:99
+#: templates/repairs/delete_part.html:10
+#: templates/repairs/delete_repair.html:14
+msgid "Delete"
+msgstr ""
+
+#: templates/accounts/calendars.html:26 templates/customers/find.html:7
+#: templates/customers/find.html.py:9
+msgid "Download"
+msgstr ""
+
+#: templates/accounts/calendars.html:47
+msgid "No calendar selected"
+msgstr ""
+
+#: templates/accounts/delete_calendar.html:9
+msgid "This will also delete all events in this calendar"
+msgstr ""
+
+#: templates/accounts/login.html:18
+msgid "Login"
+msgstr ""
+
+#: templates/accounts/logout.html:5
+msgid "Logging out?"
+msgstr ""
+
+#: templates/accounts/logout.html:9
+msgid "This will terminate your Servo session."
+msgstr ""
+
+#: templates/accounts/orders.html:7 templates/checkin/welcome.html:12
+#: templates/notes/view_note.html:33 templates/orders/customer.html:18
+#: templates/orders/index.html:7
+msgid "Create Order"
+msgstr ""
+
+#: templates/accounts/orders.html:18 templates/devices/search.html:6
+#: templates/orders/index.html:18 templates/orders/parts.html:19
+#: templates/products/index.html:49 templates/products/search.html:5
+#: templates/shipments/index.html:12
+msgid "Filter results"
+msgstr ""
+
+#: templates/accounts/orders.html:43 templates/orders/index.html:35
+msgid "Reset"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:29
+#: templates/accounts/print_calendar.html:79
+msgid "Employee"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:30
+#: templates/accounts/print_calendar.html:41
+#: templates/accounts/view_calendar.html:28
+#: templates/orders/print_confirmation.html:33
+#: templates/orders/print_confirmation.html:162
+#: templates/orders/print_confirmation.html:179
+msgid "Date"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:42
+#: templates/accounts/view_calendar.html:29
+msgid "Started At"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:43
+#: templates/accounts/view_calendar.html:30
+msgid "Finished At"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:44
+#: templates/accounts/view_calendar.html:31
+msgid "Duration"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:58
+#: templates/accounts/view_calendar.html:57
+msgid "No events found"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:63 templates/invoices/index.html:48
+#: templates/notes/form.html:25 templates/orders/dispatch.html:67
+#: templates/orders/dispatch.html.py:83
+#: templates/orders/gsx_repair_form.html:34
+#: templates/orders/list_products.html:20
+#: templates/orders/print_confirmation.html:111
+#: templates/orders/print_confirmation.html:139
+#: templates/orders/products.html:14 templates/purchases/edit_po.html:67
+#: templates/purchases/list_pos.html:59
+msgid "Total"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:75
+msgid "Manager"
+msgstr ""
+
+#: templates/accounts/register.html:6 views/account.py:326
+msgid "Register"
+msgstr ""
+
+#: templates/accounts/settings.html:12 templates/admin/locations/form.html:7
+#: templates/admin/queues/form.html:12 templates/admin/users/form.html:12
+#: templates/products/form.html:15 templates/search/spotlight.html:19
+msgid "GSX"
+msgstr ""
+
+#: templates/accounts/settings.html:39 templates/accounts/tabs.html:19
+#: templates/admin/index.html:9
+msgid "Settings"
+msgstr ""
+
+#: templates/accounts/stats.html:28
+msgid "Runrate"
+msgstr ""
+
+#: templates/accounts/stats.html:29
+msgid ""
+"Shows you how many you've been assigned to during the selected period, "
+"averaged over the time scale."
+msgstr ""
+
+#: templates/accounts/tabs.html:10 templates/accounts/updates.html:26
+#: templates/accounts/updates.html.py:64 views/account.py:401
+msgid "Updates"
+msgstr ""
+
+#: templates/accounts/updates.html:7
+msgid "Clear all"
+msgstr ""
+
+#: templates/accounts/updates.html:36
+msgid "Event"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:10 views/stats.py:243
+msgid "Day"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:11 views/stats.py:244
+msgid "Week"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:12 views/stats.py:245
+msgid "Month"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:19
+msgid "Today"
+msgstr ""
+
+#: templates/admin/index.html:46
+msgid "Systen Settings"
+msgstr ""
+
+#: templates/admin/settings.html:11 templates/admin/checklist/form.html:9
+#: templates/admin/gsx/form.html:6 templates/admin/locations/form.html:6
+#: templates/admin/queues/form.html:9 templates/admin/statuses/form.html:6
+#: templates/admin/tags/index.html:31 templates/admin/templates/form.html:6
+#: templates/admin/users/form.html:8 templates/customers/form.html:8
+#: templates/devices/form.html:8 templates/products/form.html:10
+msgid "General"
+msgstr ""
+
+#: templates/admin/settings.html:12 templates/admin/inventory/index.html:8
+#: templates/products/form.html:13
+msgid "Stock"
+msgstr ""
+
+#: templates/admin/settings.html:13
+msgid "Outgoing Mail"
+msgstr ""
+
+#: templates/admin/settings.html:14
+msgid "Incoming Mail"
+msgstr ""
+
+#: templates/admin/settings.html:15
+msgid "Text Messages"
+msgstr ""
+
+#: templates/admin/settings.html:16
+msgid "Check-in"
+msgstr ""
+
+#: templates/admin/settings.html:35
+msgid "Update prices"
+msgstr ""
+
+#: templates/admin/settings.html:37
+msgid "Applies the new settings to existing products after saving"
+msgstr ""
+
+#: templates/admin/settings.html:52 views/admin.py:622
+msgid "Notifications"
+msgstr ""
+
+#: templates/admin/settings.html:67
+msgid "Sending SMS messages will be handled by Servo"
+msgstr ""
+
+#: templates/admin/settings.html:98
+msgid "Shipping Methods"
+msgstr ""
+
+#: templates/admin/settings.html:105
+msgid "Add Method"
+msgstr ""
+
+#: templates/admin/checklist/form.html:10
+msgid "Tasks"
+msgstr ""
+
+#: templates/admin/fields/remove.html:6
+msgid "Are you sure you want to delete this field?"
+msgstr ""
+
+#: templates/admin/gsx/index.html:5
+msgid "New Account"
+msgstr ""
+
+#: templates/admin/gsx/remove.html:5
+msgid "Remove GSX account"
+msgstr ""
+
+#: templates/admin/notifications/index.html:6
+msgid "Reports"
+msgstr ""
+
+#: templates/admin/notifications/index.html:15
+msgid "Daily"
+msgstr ""
+
+#: templates/admin/notifications/index.html:16
+msgid "Weekly"
+msgstr ""
+
+#: templates/admin/notifications/index.html:17
+msgid "Monthly"
+msgstr ""
+
+#: templates/admin/notifications/index.html:22
+msgid "Repairs aging beyond limits"
+msgstr ""
+
+#: templates/admin/notifications/index.html:28
+msgid "Products below stocking limit"
+msgstr ""
+
+#: templates/admin/queues/form.html:11
+msgid "Defaults"
+msgstr ""
+
+#: templates/admin/queues/form.html:28
+msgid "Time limits"
+msgstr ""
+
+#: templates/admin/queues/form.html:46
+msgid "Add Status"
+msgstr ""
+
+#: templates/admin/queues/remove.html:5
+#, python-format
+msgid "Delete queue \"%(title)s\"?"
+msgstr ""
+
+#: templates/admin/queues/remove.html:9
+msgid "Orders in this queue will not be deleted."
+msgstr ""
+
+#: templates/admin/sites/index.html:6 views/admin.py:648
+msgid "New Site"
+msgstr ""
+
+#: templates/admin/sites/index.html:12
+msgid "Sites"
+msgstr ""
+
+#: templates/admin/sites/index.html:20
+msgid "No site selected"
+msgstr ""
+
+#: templates/admin/statuses/remove.html:5
+#, python-format
+msgid "Delete status \"%(title)s\"?"
+msgstr ""
+
+#: templates/admin/tags/index.html:25
+#, python-format
+msgid "No %(objects)s found."
+msgstr ""
+
+#: templates/admin/users/delete_group.html:4
+msgid "Really delete this group?"
+msgstr ""
+
+#: templates/admin/users/delete_group.html:5
+msgid "Group member accounts will not be deleted."
+msgstr ""
+
+#: templates/admin/users/groups.html:13
+msgid "Members"
+msgstr ""
+
+#: templates/admin/users/groups.html:31
+msgid "No groups defined"
+msgstr ""
+
+#: templates/admin/users/index.html:13 views/admin.py:675
+msgid "Upload Users"
+msgstr ""
+
+#: templates/admin/users/remove.html:5
+#, python-format
+msgid "Delete user %(user)s?"
+msgstr ""
+
+#: templates/admin/users/tabs.html:5
+msgid "Users"
+msgstr ""
+
+#: templates/checkin/choose_model.html:6
+msgid "Choose your device"
+msgstr ""
+
+#: templates/checkin/choose_model.html:7
+msgid "Please choose your device model"
+msgstr ""
+
+#: templates/checkin/choose_model.html:14
+msgid "MacBook Air"
+msgstr ""
+
+#: templates/checkin/choose_model.html:20
+msgid "MacBook"
+msgstr ""
+
+#: templates/checkin/choose_model.html:26
+msgid "MacBook Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:32
+msgid "iMac"
+msgstr ""
+
+#: templates/checkin/choose_model.html:40
+msgid "iPhone"
+msgstr ""
+
+#: templates/checkin/choose_model.html:46
+msgid "iPad"
+msgstr ""
+
+#: templates/checkin/choose_model.html:52
+msgid "Apple TV"
+msgstr ""
+
+#: templates/checkin/choose_model.html:60
+msgid "Mac Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:66
+msgid "Display"
+msgstr ""
+
+#: templates/checkin/choose_model.html:72
+msgid "Mac mini"
+msgstr ""
+
+#: templates/checkin/choose_model.html:78
+msgid "Power Mac"
+msgstr ""
+
+#: templates/checkin/choose_model.html:86
+msgid "iPod Touch"
+msgstr ""
+
+#: templates/checkin/choose_model.html:92
+msgid "iPod nano"
+msgstr ""
+
+#: templates/checkin/choose_model.html:98
+msgid "iPod Shuffle"
+msgstr ""
+
+#: templates/checkin/choose_model.html:104
+msgid "iPod Classic"
+msgstr ""
+
+#: templates/checkin/choose_model.html:112
+msgid "Keyboard or Mouse"
+msgstr ""
+
+#: templates/checkin/choose_model.html:118 views/checkin.py:283
+msgid "Power Adapter"
+msgstr ""
+
+#: templates/checkin/choose_model.html:124
+msgid "AirPort and Wireless"
+msgstr ""
+
+#: templates/checkin/choose_model.html:130
+msgid "Other Device"
+msgstr ""
+
+#: templates/checkin/customer.html:7
+msgid "Your contact details"
+msgstr ""
+
+#: templates/checkin/customer.html:8
+msgid "Please make sure your contact details are filled out correctly."
+msgstr ""
+
+#: templates/checkin/device.html:8
+msgid "Please provide additional details about your device here."
+msgstr ""
+
+#: templates/checkin/device.html:8
+msgid ""
+"Providing a username and password helps us diagnose the problem faster, "
+"especially with software-related issues."
+msgstr ""
+
+#: templates/checkin/device.html:8
+msgid "For faster service time, please upload your purchase receipt."
+msgstr ""
+
+#: templates/checkin/device.html:20
+msgid "Purchased"
+msgstr ""
+
+#: templates/checkin/device.html:24 templates/devices/get_info.html:17
+#: templates/devices/search_gsx_warranty.html:17
+#: templates/devices/summary.html:16 templates/orders/devices.html:23
+#: templates/search/results/gsx_warranty.html:17
+msgid "Configration"
+msgstr ""
+
+#: templates/checkin/device.html:28 templates/devices/get_info.html:23
+#: templates/devices/summary.html:34
+#: templates/search/results/gsx_warranty.html:22
+msgid "Activation Profile"
+msgstr ""
+
+#: templates/checkin/device.html:30 templates/devices/get_info.html:25
+#: templates/devices/search_gsx_warranty.html:26
+#: templates/devices/summary.html:36 templates/orders/devices.html:33
+#: templates/search/results/gsx_warranty.html:24
+msgid "Unlocked"
+msgstr ""
+
+#: templates/checkin/device.html:34 templates/customers/form.html:9
+#: templates/devices/form.html:9 templates/devices/get_info.html:37
+#: templates/devices/search_gsx_warranty.html:36
+#: templates/orders/devices.html:45 templates/orders/gsx_repair_form.html:40
+#: templates/products/form.html:14
+msgid "Details"
+msgstr ""
+
+#: templates/checkin/device.html:38
+#: templates/devices/search_gsx_warranty.html:40
+msgid "Onsite Coverage"
+msgstr ""
+
+#: templates/checkin/device.html:42
+#: templates/devices/search_gsx_warranty.html:44
+msgid "Contract Coverage"
+msgstr ""
+
+#: templates/checkin/device.html:57 templates/checkin/issue.html:20
+#: templates/checkin/sn.html:16
+msgid "Continue"
+msgstr ""
+
+#: templates/checkin/error.html:4
+msgid "An error occurred"
+msgstr ""
+
+#: templates/checkin/error.html:12
+msgid "Try again"
+msgstr ""
+
+#: templates/checkin/issue.html:8
+msgid "Please describe the problem in as much detail as possible."
+msgstr ""
+
+#: templates/checkin/sn.html:7
+msgid "Please enter your serial number"
+msgstr ""
+
+#: templates/checkin/sn.html:8
+msgid ""
+"You can find instructions for locating the serial number by <a href=\"http://"
+"support.apple.com/kb/ht1349\" target=\"_blank\">clicking here</a>."
+msgstr ""
+
+#: templates/checkin/sn.html:8
+msgid ""
+"Clicking \"Choose\" will let you pick the type of your device if you don't "
+"know the serial number."
+msgstr ""
+
+#: templates/checkin/sn.html:8
+msgid "If you don't provide a serial number, the service time may be longer."
+msgstr ""
+
+#: templates/checkin/sn.html:9
+msgid ""
+"When checking in an accessory, please provide the serial number of the "
+"device that the accessory is used with."
+msgstr ""
+
+#: templates/checkin/sn.html:9
+msgid ""
+"For example the serial number of your iPhone if the headset is broken, or "
+"the serial number of your Mac if you have a problem with the mouse."
+msgstr ""
+
+#: templates/checkin/sn.html:15 templates/orders/customer.html:12
+#: templates/orders/statuses.html:3
+msgid "Choose"
+msgstr ""
+
+#: templates/checkin/status-show.html:33
+msgid "Refresh"
+msgstr ""
+
+#: templates/checkin/status-show.html:34
+msgid "Return"
+msgstr ""
+
+#: templates/checkin/status.html:7
+msgid "Please enter your Service Order number"
+msgstr ""
+
+#: templates/checkin/status.html:9
+msgid ""
+"The Service Order number is an 8-digit code printed on your work confirmation"
+msgstr ""
+
+#: templates/checkin/terms.html:6
+#, python-format
+msgid "%(company)s terms of service"
+msgstr ""
+
+#: templates/checkin/thanks.html:7 views/checkin.py:228
+msgid "Done!"
+msgstr ""
+
+#: templates/checkin/thanks.html:8
+msgid "Your service order has been submitted."
+msgstr ""
+
+#: templates/checkin/thanks.html:8
+msgid "Click <strong>Print</strong> to print the confirmation."
+msgstr ""
+
+#: templates/checkin/thanks.html:8
+#, python-format
+msgid "You will be redirected to the start page in %(seconds)s seconds."
+msgstr ""
+
+#: templates/checkin/troubleshoot.html:12
+msgid "Yes"
+msgstr ""
+
+#: templates/checkin/troubleshoot.html:13
+msgid "No"
+msgstr ""
+
+#: templates/checkin/welcome.html:7
+#, python-format
+msgid "Welcome to %(location)s service check-in."
+msgstr ""
+
+#: templates/checkin/welcome.html:8
+msgid ""
+"From here you can <strong>create</strong> a service order or check the "
+"<strong>status</strong> of an existing order."
+msgstr ""
+
+#: templates/checkin/welcome.html:8
+msgid "Please choose an option below."
+msgstr ""
+
+#: templates/checkin/welcome.html:11
+msgid "Check Status"
+msgstr ""
+
+#: templates/customers/choose-list.html:6 templates/customers/find.html:91
+#: templates/customers/search.html:28
+#: templates/search/results/customers.html:24
+msgid "No customers found"
+msgstr ""
+
+#: templates/customers/choose.html:5
+msgid "Search for customer"
+msgstr ""
+
+#: templates/customers/choose.html:11
+msgid "Name, email or phone number"
+msgstr ""
+
+#: templates/customers/delete_group.html:5
+msgid "Delete customer group?"
+msgstr ""
+
+#: templates/customers/delete_group.html:9
+msgid "This action will not delete the customers in this group."
+msgstr ""
+
+#: templates/customers/find.html:15 templates/customers/index.html:43
+#: templates/devices/find.html:7 templates/devices/index.html:19
+#: templates/generic/index.html:6 templates/notes/find.html:8
+#: templates/notes/list_notes.html:28
+msgid "Browse"
+msgstr ""
+
+#: templates/customers/find.html:73 templates/devices/find.html:53
+#: templates/devices/list.html:26 templates/products/get_info.html:55
+msgid "View"
+msgstr ""
+
+#: templates/customers/index.html:11 templates/customers/index.html.py:13
+msgid "New Contact"
+msgstr ""
+
+#: templates/customers/index.html:24 templates/devices/index.html:13
+msgid "Upload"
+msgstr ""
+
+#: templates/customers/index.html:27 templates/customers/index.html.py:30
+#: views/admin.py:412
+msgid "Edit Group"
+msgstr ""
+
+#: templates/customers/index.html:28 templates/customers/index.html.py:31
+msgid "Delete Group"
+msgstr ""
+
+#: templates/customers/index.html:57 templates/devices/specs.html:7
+#: templates/products/index.html:61 templates/products/view.html:11
+#: views/product.py:36 views/product.py:326
+msgid "All"
+msgstr ""
+
+#: templates/customers/index.html:74
+msgid "No customer selected"
+msgstr ""
+
+#: templates/customers/merge.html:12 templates/customers/move.html:12
+msgid "Customer name"
+msgstr ""
+
+#: templates/customers/merge.html:18 templates/customers/move.html:19
+#: templates/orders/close.html:15 templates/orders/toolbar.html:69
+#: templates/orders/toolbar.html.py:73 templates/products/choose.html:17
+#: templates/snippets/modal.html:11
+msgid "Close"
+msgstr ""
+
+#: templates/customers/move.html:5
+msgid "Move customer"
+msgstr ""
+
+#: templates/customers/move.html:18
+msgid "Move to top"
+msgstr ""
+
+#: templates/customers/remove.html:9
+msgid ""
+"This will also delete all customer's contacts. Customer's orders will not be "
+"deleted"
+msgstr ""
+
+#: templates/customers/upload.html:5
+msgid "Upload customer data"
+msgstr ""
+
+#: templates/customers/view.html:48
+msgid "Contacts"
+msgstr ""
+
+#: templates/customers/view.html:55
+msgid "Use in "
+msgstr ""
+
+#: templates/customers/view.html:57 templates/devices/view.html:22
+msgid "Use in current order"
+msgstr ""
+
+#: templates/customers/view.html:61 templates/devices/find.html:50
+#: templates/devices/list.html:23
+#: templates/devices/search_gsx_warranty.html:58
+#: templates/devices/view.html:25
+#: templates/search/results/gsx_warranty.html:33
+msgid "Create Service Order"
+msgstr ""
+
+#: templates/customers/view.html:69 templates/customers/view.html.py:72
+msgid "Move Customer"
+msgstr ""
+
+#: templates/customers/view.html:70 templates/customers/view.html.py:73
+msgid "Merge Customer"
+msgstr ""
+
+#: templates/customers/view.html:108 templates/notes/list_notes.html:57
+msgid "No messages found"
+msgstr ""
+
+#: templates/devices/accessories_edit.html:4
+msgid "Manage Accessories"
+msgstr ""
+
+#: templates/devices/accessories_edit.html:15
+#: templates/orders/customer.html:22 templates/orders/customer.html.py:24
+#: templates/orders/devices.html:120 templates/orders/followers.html:23
+#: templates/orders/products.html:77 templates/orders/remove_customer.html:15
+#: templates/orders/remove_device.html:15
+msgid "Remove"
+msgstr ""
+
+#: templates/devices/accessories_edit.html:19
+msgid "No accessories added"
+msgstr ""
+
+#: templates/devices/choose-list.html:10 templates/devices/find.html:62
+#: templates/devices/search_gsx_repairs.html:24
+#: templates/search/results/gsx_notfound.html:3
+msgid "No search results"
+msgstr ""
+
+#: templates/devices/choose.html:5
+msgid "Search for device"
+msgstr ""
+
+#: templates/devices/choose.html:11
+msgid "Serial number or IMEI code"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:4
+msgid "Result"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:6
+msgid "Tool"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:8
+msgid "Passed modules"
+msgstr ""
+
+#: templates/devices/diagnostics.html:4
+msgid "Diagnostics"
+msgstr ""
+
+#: templates/devices/diagnostics.html:11
+msgid "Fetching diagnostics..."
+msgstr ""
+
+#: templates/devices/find.html:29 templates/devices/search_gsx_repairs.html:7
+#: templates/notes/view_note.html:23 templates/orders/list.html:12
+#: templates/products/view.html:132 templates/products/view.html.py:156
+#: templates/products/view.html:181 templates/purchases/list_pos.html:52
+msgid "Created"
+msgstr ""
+
+#: templates/devices/find.html:47 templates/devices/list.html:19
+#: templates/devices/view.html:20 templates/products/view.html:93
+msgid "Use in"
+msgstr ""
+
+#: templates/devices/find.html:51 templates/devices/list.html:24
+msgid "Show Service Orders"
+msgstr ""
+
+#: templates/devices/form.html:27 templates/products/form.html:53
+msgid "Enter tag"
+msgstr ""
+
+#: templates/devices/get_info.html:12
+#: templates/devices/search_gsx_warranty.html:13
+#: templates/devices/summary.html:11 templates/orders/devices.html:18
+#: templates/search/results/gsx_warranty.html:13
+msgid "Purchase Date"
+msgstr ""
+
+#: templates/devices/index.html:8 templates/devices/index.html.py:11
+msgid "Show Parts"
+msgstr ""
+
+#: templates/devices/index.html:36 templates/orders/print_confirmation.html:64
+msgid "Model"
+msgstr ""
+
+#: templates/devices/index.html:40
+msgid "No Product Line selected"
+msgstr ""
+
+#: templates/devices/index.html:58 templates/devices/list.html:35
+#: templates/search/results/devices.html:8
+msgid "No devices found"
+msgstr ""
+
+#: templates/devices/index.html:68
+msgid "No device selected"
+msgstr ""
+
+#: templates/devices/parts.html:9 templates/shipments/edit_bulk_return.html:56
+#: templates/shipments/list_incoming.html:17
+msgid "Part"
+msgstr ""
+
+#: templates/devices/parts.html:21
+#, python-format
+msgid "Substituted to %(code)s"
+msgstr ""
+
+#: templates/devices/parts.html:27 templates/products/choose-list.html:7
+#: templates/products/list_rows.html:54 templates/products/search.html:13
+#: templates/search/results/products.html:8
+msgid "No products found"
+msgstr ""
+
+#: templates/devices/remove.html:5
+#, python-format
+msgid "Delete device %(sn)s\"?"
+msgstr ""
+
+#: templates/devices/remove.html:9
+msgid "This will also remove it from all service orders."
+msgstr ""
+
+#: templates/devices/search_gsx.html:13 templates/devices/search_gsx.html:15
+#: templates/devices/search_gsx_error.html:10
+#: templates/orders/gsx_repair_form.html:31
+#: templates/repairs/get_details.html:22 templates/search/results/gsx.html:13
+#: templates/search/results/gsx.html:15
+#: templates/shipments/list_bulk_returns.html:12
+msgid "Parts"
+msgstr ""
+
+#: templates/devices/search_gsx.html:18 templates/devices/search_gsx.html:20
+#: templates/devices/search_gsx_error.html:11
+#: templates/search/results/gsx.html:18 templates/search/results/gsx.html:20
+msgid "Repairs"
+msgstr ""
+
+#: templates/devices/search_gsx_parts.html:20
+#: templates/devices/search_gsx_parts.html:22
+msgid "Create Product"
+msgstr ""
+
+#: templates/devices/search_gsx_repairs.html:5
+msgid "Number"
+msgstr ""
+
+#: templates/devices/search_gsx_warranty.html:31
+#: templates/purchases/view_po.html:19
+msgid "Sales Order"
+msgstr ""
+
+#: templates/devices/search_gsx_warranty.html:55
+#: templates/search/results/gsx_warranty.html:35
+msgid "Use in order"
+msgstr ""
+
+#: templates/devices/search_gsx_warranty.html:60
+#: templates/search/results/gsx_warranty.html:39
+#: templates/search/results/gsx_warranty.html:41
+msgid "Download Manual"
+msgstr ""
+
+#: templates/devices/specs.html:8
+msgid "Specs"
+msgstr ""
+
+#: templates/devices/specs.html:10
+msgid "Tagi"
+msgstr ""
+
+#: templates/devices/specs.html:17
+msgid "Luo malli"
+msgstr ""
+
+#: templates/devices/upload_devices.html:5
+msgid "Upload devices"
+msgstr ""
+
+#: templates/devices/view.html:10 templates/devices/view.html.py:12
+msgid "GSX Repairs"
+msgstr ""
+
+#: templates/devices/view.html:33 templates/devices/view.html.py:35
+msgid "Service Manual"
+msgstr ""
+
+#: templates/devices/view.html:44 templates/devices/view.html.py:47
+#: templates/orders/devices.html:93
+msgid "Update Warranty Status"
+msgstr ""
+
+#: templates/devices/view.html:45 templates/devices/view.html.py:48
+msgid "Fetch Diagnostics"
+msgstr ""
+
+#: templates/devices/view.html:73
+msgid "Fetching repairs..."
+msgstr ""
+
+#: templates/generic/index.html:22
+msgid "Create New"
+msgstr ""
+
+#: templates/invoices/index.html:7 templates/invoices/index.html.py:105
+#: templates/products/tabs.html:17 templates/products/view.html:46
+#: views/invoices.py:23
+msgid "Invoices"
+msgstr ""
+
+#: templates/invoices/index.html:8
+msgid "Sales Invoices"
+msgstr ""
+
+#: templates/invoices/index.html:9
+msgid "Apple Invoices"
+msgstr ""
+
+#: templates/invoices/index.html:46
+msgid "Date Created"
+msgstr ""
+
+#: templates/invoices/index.html:49
+msgid "Date Paid"
+msgstr ""
+
+#: templates/invoices/index.html:71
+msgid "Print receipt"
+msgstr ""
+
+#: templates/invoices/index.html:78
+msgid "No invoices found"
+msgstr ""
+
+#: templates/notes/form.html:38
+msgid "Attachments"
+msgstr ""
+
+#: templates/notes/form.html:39
+msgid "Labels"
+msgstr ""
+
+#: templates/notes/form.html:40
+msgid "Escalation"
+msgstr ""
+
+#: templates/notes/form.html:110
+msgid "Edit Note"
+msgstr ""
+
+#: templates/notes/list_notes.html:11
+msgid "New Message"
+msgstr ""
+
+#: templates/notes/list_notes.html:12
+msgid "New Escalation"
+msgstr ""
+
+#: templates/notes/list_notes.html:37
+msgid "Inbox"
+msgstr ""
+
+#: templates/notes/list_notes.html:38
+msgid "Flagged"
+msgstr ""
+
+#: templates/notes/list_notes.html:39
+msgid "Sent"
+msgstr ""
+
+#: templates/notes/list_notes.html:40
+msgid "Escalations"
+msgstr ""
+
+#: templates/notes/list_notes.html:65
+msgid "No message selected"
+msgstr ""
+
+#: templates/notes/messages.html:5 templates/orders/notes.html:48
+msgid "Message Log"
+msgstr ""
+
+#: templates/notes/messages.html:18
+msgid "No messages to display"
+msgstr ""
+
+#: templates/notes/remove.html:5
+msgid "Delete this note?"
+msgstr ""
+
+#: templates/notes/remove.html:9
+msgid "This will also delete any replies to this note."
+msgstr ""
+
+#: templates/notes/search-results.html:21
+msgid "No notes found"
+msgstr ""
+
+#: templates/notes/search.html:7
+msgid "Create Note"
+msgstr ""
+
+#: templates/notes/view_note.html:24
+msgid "Subject"
+msgstr ""
+
+#: templates/notes/view_note.html:35 templates/orders/notes.html:21
+msgid "Reply"
+msgstr ""
+
+#: templates/orders/close.html:5
+msgid "Close order"
+msgstr ""
+
+#: templates/orders/close.html:9
+msgid "This order will no longer be editable."
+msgstr ""
+
+#: templates/orders/close_repair.html:5
+msgid "Complete the GSX repair?"
+msgstr ""
+
+#: templates/orders/close_repair.html:9
+msgid "The GSX repair will no longer be editable."
+msgstr ""
+
+#: templates/orders/close_repair.html:15
+msgid "Complete"
+msgstr ""
+
+#: templates/orders/customer.html:14 templates/orders/customer.html.py:16
+msgid "Add contact"
+msgstr ""
+
+#: templates/orders/delete_order.html:5
+#, python-format
+msgid "Really delete order %(id)s? "
+msgstr ""
+
+#: templates/orders/delete_order.html:9
+msgid ""
+"This will also delete all the order's notes. This action cannot be undone."
+msgstr ""
+
+#: templates/orders/devices.html:61 templates/orders/devices.html.py:63
+msgid "View Parts"
+msgstr ""
+
+#: templates/orders/devices.html:63
+msgid "Assign order to a queue first"
+msgstr ""
+
+#: templates/orders/devices.html:65
+msgid "View History"
+msgstr ""
+
+#: templates/orders/devices.html:67 templates/orders/devices.html.py:69
+msgid "View Accessories"
+msgstr ""
+
+#: templates/orders/devices.html:78 templates/orders/devices.html.py:80
+msgid "Create Carry-In Repair"
+msgstr ""
+
+#: templates/orders/devices.html:80
+msgid "Device has open GSX repairs"
+msgstr ""
+
+#: templates/orders/devices.html:83 templates/orders/devices.html.py:85
+msgid "Create Onsite Repair"
+msgstr ""
+
+#: templates/orders/devices.html:88 templates/orders/devices.html.py:90
+msgid "Create Whole-Unit Exchange"
+msgstr ""
+
+#: templates/orders/devices.html:94
+msgid "Get Diagnostics"
+msgstr ""
+
+#: templates/orders/devices.html:97 templates/orders/devices.html.py:99
+#: templates/orders/toolbar.html:91
+msgid "Show Barcode"
+msgstr ""
+
+#: templates/orders/devices.html:102
+msgid "Show Service Manual"
+msgstr ""
+
+#: templates/orders/devices.html:105
+msgid "Show Exploded View"
+msgstr ""
+
+#: templates/orders/devices.html:112 templates/orders/devices.html.py:114
+msgid "Report"
+msgstr ""
+
+#: templates/orders/devices.html:131
+msgid "Order doesn't contain any devices"
+msgstr ""
+
+#: templates/orders/devices.html:136 templates/orders/toolbar.html:14
+msgid "Add Device"
+msgstr ""
+
+#: templates/orders/dispatch.html:40
+msgid "Payment"
+msgstr ""
+
+#: templates/orders/dispatch.html:58 templates/products/index.html:100
+#: templates/products/search.html:18 templates/products/tabs.html:8
+#: templates/products/view.html:25 templates/purchases/edit_po.html:21
+#: templates/purchases/list_pos.html:58 views/product.py:31
+msgid "Products"
+msgstr ""
+
+#: templates/orders/dispatch.html:64
+#: templates/orders/print_confirmation.html:108
+#: templates/orders/print_dispatch.html:16
+msgid "Qty"
+msgstr ""
+
+#: templates/orders/dispatch.html:65
+#: templates/orders/print_confirmation.html:110
+#: templates/orders/products.html:13 templates/products/view.html:131
+#: templates/products/view.html.py:157 templates/products/view.html:182
+msgid "Price"
+msgstr ""
+
+#: templates/orders/dispatch.html:66
+msgid "Price w/ tax"
+msgstr ""
+
+#: templates/orders/dispatch.html:97 templates/orders/dispatch.html.py:106
+#: templates/orders/toolbar.html:64 templates/orders/toolbar.html.py:66
+#: templates/purchases/edit_po.html:22
+msgid "Dispatch"
+msgstr ""
+
+#: templates/orders/edit.html:23
+msgid "Checkin Location"
+msgstr ""
+
+#: templates/orders/edit.html:43
+msgid "Checkout Location"
+msgstr ""
+
+#: templates/orders/edit.html:69
+msgid "Select Customer"
+msgstr ""
+
+#: templates/orders/edit.html:78
+msgid "Place"
+msgstr ""
+
+#: templates/orders/edit.html:79
+msgid "Enter place"
+msgstr ""
+
+#: templates/orders/edit.html:86
+msgid "No place"
+msgstr ""
+
+#: templates/orders/edit.html:92
+msgid "Enter label"
+msgstr ""
+
+#: templates/orders/edit.html:97
+msgid "No labels"
+msgstr ""
+
+#: templates/orders/followers.html:5
+msgid "Followers"
+msgstr ""
+
+#: templates/orders/followers.html:6
+msgid "Enter name"
+msgstr ""
+
+#: templates/orders/followers.html:19 templates/orders/followers.html.py:21
+msgid "Make primary"
+msgstr ""
+
+#: templates/orders/followers.html:27
+msgid "No followers"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:32
+msgid "Checking warranty"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:39
+msgid "Repair"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:42
+msgid "Components"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:73
+#: templates/purchases/edit_po.html:85
+#: templates/shipments/edit_bulk_return.html:94
+msgid "Confirm"
+msgstr ""
+
+#: templates/orders/list.html:22
+msgid "No customer"
+msgstr ""
+
+#: templates/orders/list.html:22
+msgid "No description"
+msgstr ""
+
+#: templates/orders/list.html:29
+msgid "Nobody"
+msgstr ""
+
+#: templates/orders/list.html:31
+msgid "No status"
+msgstr ""
+
+#: templates/orders/list.html:36 templates/orders/search.html:6
+#: templates/search/results/orders.html:8
+msgid "No orders found"
+msgstr ""
+
+#: templates/orders/list_products.html:12
+msgid "Click to select EEE code"
+msgstr ""
+
+#: templates/orders/list_products.html:16
+msgid "No products added"
+msgstr ""
+
+#: templates/orders/notes.html:8
+msgid "Order doesn't contain any notes."
+msgstr ""
+
+#: templates/orders/notes.html:40
+msgid "Mark"
+msgstr ""
+
+#: templates/orders/notes.html:47 templates/orders/repair.html:10
+msgid "Copy"
+msgstr ""
+
+#: templates/orders/notes.html:65
+msgid "Note has been sent"
+msgstr ""
+
+#: templates/orders/notes.html:83 templates/orders/toolbar.html:6
+#: templates/orders/toolbar.html.py:8
+msgid "Add Note"
+msgstr ""
+
+#: templates/orders/parts.html:15
+msgid "Parts and Services"
+msgstr ""
+
+#: templates/orders/parts.html:28
+msgid "Fetching parts..."
+msgstr ""
+
+#: templates/orders/parts.html:35 templates/stats/sales.html:21
+msgid "Service Parts"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:26
+msgid "Repair Number"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:37
+msgid "Work Confirmation"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:44
+msgid "Customer Information"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:45
+msgid "Product Information"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:53
+#: templates/orders/print_receipt.html:11
+msgid "Daytime Phone"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:66
+msgid "Estimated Date of Purchase"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:69
+msgid "Activation Policy"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:89
+msgid "Work Description"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:101
+#: templates/orders/print_dispatch.html:9
+msgid "Products and Services"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:107
+#: templates/orders/print_dispatch.html:15
+#, python-format
+msgid "VAT %%"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:109
+#, python-format
+msgid "0%% VAT"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:119
+#: templates/orders/print_dispatch.html:24
+#: templates/orders/print_receipt.html:23
+msgid "SN"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:151
+msgid "Customer Pickup"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:160
+msgid "Customer Name"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:161
+#: templates/orders/print_confirmation.html:178
+msgid "Signature"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:168
+msgid "Received By"
+msgstr ""
+
+#: templates/orders/print_dispatch.html:5
+#, python-format
+msgid "Dispatch #%(code)s"
+msgstr ""
+
+#: templates/orders/print_dispatch.html:25
+msgid "mon."
+msgstr ""
+
+#: templates/orders/print_receipt.html:6
+#, python-format
+msgid "Receipt %(number)s"
+msgstr ""
+
+#: templates/orders/print_receipt.html:40
+msgid "Date of invoice"
+msgstr ""
+
+#: templates/orders/print_receipt.html:41
+msgid "Payment method"
+msgstr ""
+
+#: templates/orders/print_receipt.html:42
+msgid "Sales Person"
+msgstr ""
+
+#: templates/orders/products.html:11 templates/purchases/edit_po.html:34
+#: templates/purchases/view_po.html:33
+msgid "Amount"
+msgstr ""
+
+#: templates/orders/products.html:12
+msgid "Price Category"
+msgstr ""
+
+#: templates/orders/products.html:43 templates/orders/products.html.py:45
+msgid "Add to Repair"
+msgstr ""
+
+#: templates/orders/products.html:72 templates/orders/products.html.py:74
+msgid "Create Device"
+msgstr ""
+
+#: templates/orders/products.html:86
+msgid "Order doesn't contain any products."
+msgstr ""
+
+#: templates/orders/products.html:91 templates/orders/toolbar.html:15
+#: templates/purchases/edit_po.html:6
+msgid "Add Product"
+msgstr ""
+
+#: templates/orders/products.html:96
+msgid "Order Total"
+msgstr ""
+
+#: templates/orders/remove_customer.html:5
+msgid "Remove this customer from the order?"
+msgstr ""
+
+#: templates/orders/remove_customer.html:9
+msgid "This will not delete the customer from the database."
+msgstr ""
+
+#: templates/orders/remove_device.html:5
+#, python-format
+msgid "Remove %(dev)s?"
+msgstr ""
+
+#: templates/orders/remove_device.html:9
+msgid "This will not delete the device from the database."
+msgstr ""
+
+#: templates/orders/remove_product.html:5
+msgid "Really remove product from order?"
+msgstr ""
+
+#: templates/orders/remove_product.html:9
+msgid "Product will not be deleted from the warehouse."
+msgstr ""
+
+#: templates/orders/repair.html:6 templates/orders/repair.html.py:8
+msgid "Complete Repair"
+msgstr ""
+
+#: templates/orders/repair.html:16
+msgid "Symptom"
+msgstr ""
+
+#: templates/orders/repair.html:18
+msgid "Diagnosis"
+msgstr ""
+
+#: templates/orders/repair.html:20
+msgid "Request Review"
+msgstr ""
+
+#: templates/orders/reserve_products.html:5
+#, python-format
+msgid "Reserve all products in order %(id)s?"
+msgstr ""
+
+#: templates/orders/reserve_products.html:11
+msgid "Reserve"
+msgstr ""
+
+#: templates/orders/tabs.html:8
+msgid "Events"
+msgstr ""
+
+#: templates/orders/toolbar.html:16
+msgid "Choose Customer"
+msgstr ""
+
+#: templates/orders/toolbar.html:25
+msgid "Cost Estimate"
+msgstr ""
+
+#: templates/orders/toolbar.html:27
+msgid "Receipt"
+msgstr ""
+
+#: templates/orders/toolbar.html:28
+msgctxt "noun"
+msgid "Dispatch"
+msgstr ""
+
+#: templates/orders/toolbar.html:42 templates/orders/toolbar.html.py:44
+msgid "Remove Queue"
+msgstr ""
+
+#: templates/orders/toolbar.html:59
+msgid "Remove Status"
+msgstr ""
+
+#: templates/orders/toolbar.html:71
+msgid "Reopen"
+msgstr ""
+
+#: templates/orders/toolbar.html:82
+msgid "Unfollow Order"
+msgstr ""
+
+#: templates/orders/toolbar.html:84
+msgid "Follow Order"
+msgstr ""
+
+#: templates/orders/toolbar.html:87 templates/orders/toolbar.html.py:89
+msgid "Copy Order"
+msgstr ""
+
+#: templates/orders/toolbar.html:94
+msgid "Priority"
+msgstr ""
+
+#: templates/orders/toolbar.html:103 templates/orders/toolbar.html.py:106
+msgid "Reserve Products"
+msgstr ""
+
+#: templates/orders/toolbar.html:104 templates/orders/toolbar.html.py:107
+msgid "Order Products"
+msgstr ""
+
+#: templates/orders/toolbar.html:111 templates/orders/toolbar.html.py:113
+msgid "Delete Order"
+msgstr ""
+
+#: templates/products/category_form.html:5
+msgid "Edit Product Group"
+msgstr ""
+
+#: templates/products/choose.html:5
+msgid "Search for a product"
+msgstr ""
+
+#: templates/products/choose.html:11
+msgid "Code, title or category"
+msgstr ""
+
+#: templates/products/delete_category.html:5
+msgid "Delete product category?"
+msgstr ""
+
+#: templates/products/delete_category.html:9
+msgid "This action will not delete the products in this category."
+msgstr ""
+
+#: templates/products/form.html:42
+msgid "Add"
+msgstr ""
+
+#: templates/products/get_info.html:6
+msgid "Product Info"
+msgstr ""
+
+#: templates/products/get_info.html:26 templates/products/view.html:67
+msgid "Component Group"
+msgstr ""
+
+#: templates/products/get_info.html:31
+msgid "EEE Code"
+msgstr ""
+
+#: templates/products/get_info.html:40 templates/products/view.html:82
+msgid "Stocked"
+msgstr ""
+
+#: templates/products/get_info.html:42 templates/products/receive_item.html:15
+#: templates/products/view.html:84 templates/purchases/list_pos.html:53
+#: templates/shipments/list_incoming.html:20
+msgid "Ordered"
+msgstr ""
+
+#: templates/products/get_info.html:44 templates/products/view.html:86
+msgid "Reserved"
+msgstr ""
+
+#: templates/products/get_info.html:49
+msgid "Product not in inventory"
+msgstr ""
+
+#: templates/products/index.html:15 templates/products/index.html.py:18
+msgid "New Sales Order"
+msgstr ""
+
+#: templates/products/index.html:16 templates/products/index.html.py:19
+msgid "New Purchase Order"
+msgstr ""
+
+#: templates/products/index.html:30 templates/products/index.html.py:33
+msgid "Edit Category"
+msgstr ""
+
+#: templates/products/index.html:31 templates/products/index.html.py:34
+msgid "Delete Category"
+msgstr ""
+
+#: templates/products/index.html:37
+msgid "Download Products"
+msgstr ""
+
+#: templates/products/index.html:38
+msgid "Upload Products"
+msgstr ""
+
+#: templates/products/index.html:39
+msgid "Upload Parts Database"
+msgstr ""
+
+#: templates/products/index_outgoing.html:10
+#: templates/products/index_outgoing.html:55
+msgid "Outgoing"
+msgstr ""
+
+#: templates/products/index_outgoing.html:11
+msgid "Shipped"
+msgstr ""
+
+#: templates/products/index_outgoing.html:23
+#: templates/purchases/edit_po.html:31
+#: templates/shipments/list_returns.html:12
+msgid "Code"
+msgstr ""
+
+#: templates/products/index_outgoing.html:26
+#: templates/shipments/edit_bulk_return.html:58
+#: templates/shipments/list_returns.html:15
+msgid "Return Order"
+msgstr ""
+
+#: templates/products/index_outgoing.html:27
+#: templates/shipments/list_returns.html:16
+msgid "Register For Return"
+msgstr ""
+
+#: templates/products/index_outgoing.html:44
+#: templates/shipments/list_returns.html:33
+msgid "No parts pending return"
+msgstr ""
+
+#: templates/products/list.html:10
+msgid "In Stock"
+msgstr ""
+
+#: templates/products/list_rows.html:31
+msgid "Use in Service Order"
+msgstr ""
+
+#: templates/products/list_rows.html:35 templates/products/view.html:96
+msgid "Create Sales Order"
+msgstr ""
+
+#: templates/products/list_rows.html:36
+msgid "Create Purchase Order"
+msgstr ""
+
+#: templates/products/list_rows.html:39
+msgid "Use in Purchase Order"
+msgstr ""
+
+#: templates/products/receive_item.html:21
+msgid "Stocking Order"
+msgstr ""
+
+#: templates/products/receive_item.html:49
+#: templates/shipments/list_incoming.html:52
+msgid "Receive"
+msgstr ""
+
+#: templates/products/remove.html:5
+#, python-format
+msgid "Really delete product \"%(code)s\"?"
+msgstr ""
+
+#: templates/products/tabs.html:11
+msgid "Shipments"
+msgstr ""
+
+#: templates/products/tabs.html:14 templates/purchases/list_pos.html:131
+#: templates/purchases/view_po.html:55 views/purchases.py:28
+msgid "Purchase Orders"
+msgstr ""
+
+#: templates/products/upload_gsx_parts.html:5
+msgid "Upload GSX Parts"
+msgstr ""
+
+#: templates/products/view.html:44 templates/stats/index.html:12
+#: templates/stats/locations.html:9 templates/stats/queues.html:9
+#: templates/stats/sales.html:9 templates/stats/sales.html.py:13
+#: templates/stats/statuses.html:9
+msgid "Sales"
+msgstr ""
+
+#: templates/products/view.html:45 templates/stats/sales.html:17
+msgid "Purchases"
+msgstr ""
+
+#: templates/products/view.html:104 templates/products/view.html.py:106
+#: templates/products/view.html:111
+msgid "Update Price"
+msgstr ""
+
+#: templates/products/view.html:133
+msgid "Dispatched"
+msgstr ""
+
+#: templates/products/view.html:147
+msgid "No Sales Orders"
+msgstr ""
+
+#: templates/products/view.html:171 templates/purchases/list_pos.html:106
+msgid "No Purchase Orders"
+msgstr ""
+
+#: templates/products/view.html:195
+msgid "No invoices"
+msgstr ""
+
+#: templates/purchases/edit_po.html:61
+msgid "Order does not contain any products"
+msgstr ""
+
+#: templates/purchases/list_pos.html:7
+msgid "New"
+msgstr ""
+
+#: templates/purchases/list_pos.html:86 templates/purchases/list_pos.html:88
+msgid "Create GSX Stocking Order"
+msgstr ""
+
+#: templates/purchases/order_stock.html:5
+msgid "Submit this stocking order?"
+msgstr ""
+
+#: templates/purchases/order_stock.html:9
+msgid "Products will be ordered from GSX."
+msgstr ""
+
+#: templates/purchases/view_po.html:15
+msgid "Created By"
+msgstr ""
+
+#: templates/purchases/view_po.html:17
+msgid "Created At"
+msgstr ""
+
+#: templates/purchases/view_po.html:35
+msgid "Received At"
+msgstr ""
+
+#: templates/repairs/add_part.html:5
+#, python-format
+msgid "Add part to repair %(repair)s?"
+msgstr ""
+
+#: templates/repairs/delete_part.html:5
+msgid "Remove this part from the GSX repair?"
+msgstr ""
+
+#: templates/repairs/delete_repair.html:5
+msgid "Delete this GSX repair?"
+msgstr ""
+
+#: templates/repairs/delete_repair.html:9
+msgid "Only repairs that have not been submitted can be deleted."
+msgstr ""
+
+#: templates/repairs/get_details.html:12
+#: templates/search/results/gsx_repair_details.html:8
+msgid "CS Code"
+msgstr ""
+
+#: templates/repairs/get_details.html:14
+#: templates/search/results/gsx_repair_details.html:10
+msgid "Tracking Number"
+msgstr ""
+
+#: templates/repairs/get_details.html:18
+#: templates/search/results/gsx_repair_details.html:14
+msgid "Warranty Coverage"
+msgstr ""
+
+#: templates/repairs/part_menu.html:4 templates/repairs/part_menu.html.py:7
+msgid "Print Return Label"
+msgstr ""
+
+#: templates/repairs/part_menu.html:7
+msgid "Part has no return order number"
+msgstr ""
+
+#: templates/repairs/part_menu.html:11 templates/repairs/part_menu.html:13
+msgid "Update Serial Numbers"
+msgstr ""
+
+#: templates/repairs/part_menu.html:17 templates/repairs/part_menu.html:27
+msgid "Return DOA"
+msgstr ""
+
+#: templates/repairs/part_menu.html:18 templates/repairs/part_menu.html:28
+#: views/shipments.py:326
+msgid "Return Good Part"
+msgstr ""
+
+#: templates/repairs/part_menu.html:19 templates/repairs/part_menu.html:29
+#: views/shipments.py:330
+msgid "Convert to Stock"
+msgstr ""
+
+#: templates/repairs/part_menu.html:22 templates/repairs/part_menu.html:24
+#: templates/repairs/part_menu.html:31
+msgid "Remove from Repair"
+msgstr ""
+
+#: templates/search/spotlight.html:11
+msgid "Results"
+msgstr ""
+
+#: templates/search/spotlight.html:25
+msgid "Products and Parts"
+msgstr ""
+
+#: templates/search/spotlight.html:28
+msgid "Articles"
+msgstr ""
+
+#: templates/search/spotlight.html:36
+msgid "Home"
+msgstr ""
+
+#: templates/shipments/add_to_return-results.html:6
+msgid "No parts found"
+msgstr ""
+
+#: templates/shipments/add_to_return.html:5
+msgid "Add part to return"
+msgstr ""
+
+#: templates/shipments/add_to_return.html:11
+msgid "Return order number"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:6
+msgid "Add Part"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:9
+#: templates/shipments/edit_bulk_return.html:11
+msgid "Verify"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:28
+#: templates/shipments/view_bulk_return.html:6
+msgid "Carrier"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:34
+#: templates/shipments/view_bulk_return.html:8
+msgid "Tracking"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:40
+#: templates/shipments/view_bulk_return.html:10
+msgid "Dimensions"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:46
+#: templates/shipments/view_bulk_return.html:12
+msgid "Weight"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:59
+msgid "Overpack"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:85
+#: templates/shipments/view_bulk_return.html:28
+msgid "No parts registered for this shipment"
+msgstr ""
+
+#: templates/shipments/index.html:26 templates/shipments/list_incoming.html:60
+msgid "Incoming"
+msgstr ""
+
+#: templates/shipments/index.html:29 templates/shipments/list_returns.html:43
+msgid "Parts Pending Return"
+msgstr ""
+
+#: templates/shipments/index.html:31
+#: templates/shipments/list_bulk_returns.html:36
+msgid "Browse Returns"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:9
+msgid "ID"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:11
+msgid "Tracking URL"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:13
+msgid "Returned"
+msgstr ""
+
+#: templates/shipments/list_incoming.html:46
+msgid "No incoming products"
+msgstr ""
+
+#: templates/shipments/submit_bulk_return.html:4
+msgid "Submit the bulk return?"
+msgstr ""
+
+#: templates/shipments/view_bulk_return.html:15
+msgid "Open Packing List"
+msgstr ""
+
+#: templates/snippets/error_modal.html:5
+msgid "An error occured..."
+msgstr ""
+
+#: templates/stats/index.html:8 templates/stats/locations.html:5
+#: templates/stats/queues.html:5 templates/stats/sales.html:5
+#: templates/stats/statuses.html:5
+msgid "Technicians"
+msgstr ""
+
+#: templates/stats/index.html:40
+msgid "Orders Assigned"
+msgstr ""
+
+#: templates/stats/index.html:41
+msgid ""
+"Shows how many new orders have been assigned to each technician over the "
+"given time period."
+msgstr ""
+
+#: templates/stats/index.html:44 templates/stats/locations.html:13
+#: templates/stats/queues.html:13
+msgid "Orders Created"
+msgstr ""
+
+#: templates/stats/index.html:45
+msgid "This graph shows how many orders are checked in by each user."
+msgstr ""
+
+#: templates/stats/index.html:48 templates/stats/locations.html:29
+#: templates/stats/queues.html:25
+msgid "Work Distribution"
+msgstr ""
+
+#: templates/stats/index.html:49
+msgid ""
+"Shows you how the total number of service orders is distributed across the "
+"technicians at this location."
+msgstr ""
+
+#: templates/stats/locations.html:14
+msgid "Shows you how many orders are created at each location."
+msgstr ""
+
+#: templates/stats/locations.html:17 templates/stats/queues.html:17
+msgid "Orders Closed"
+msgstr ""
+
+#: templates/stats/locations.html:18
+msgid "Shows you how many orders have been closed at each location."
+msgstr ""
+
+#: templates/stats/locations.html:21 templates/stats/queues.html:21
+msgid "Average Turnaround"
+msgstr ""
+
+#: templates/stats/locations.html:22
+msgid "Shows how many hours it takes to complete an order at each location."
+msgstr ""
+
+#: templates/stats/locations.html:25
+msgid "Average Runrate"
+msgstr ""
+
+#: templates/stats/locations.html:26
+msgid "Shows you how many orders people are working on at each location."
+msgstr ""
+
+#: templates/stats/locations.html:30
+msgid ""
+"This shows you how your overall work load is distributed across your service "
+"locations."
+msgstr ""
+
+#: templates/stats/queues.html:14
+msgid ""
+"This is your total number of orders per queue in the specified time period"
+msgstr ""
+
+#: templates/stats/queues.html:18
+msgid "Shows you how many orders have been closed in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:22
+msgid "Shows how many hours it takes to complete an order in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:26
+msgid ""
+"This shows your total ratio of orders over the time period distributed over "
+"each queue."
+msgstr ""
+
+#: templates/stats/sales.html:14
+msgid "Shows you invoice totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:18
+msgid ""
+"Shows you Purchase Order totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:22
+msgid "Shows you how many parts have been ordered for each labour tier."
+msgstr ""
+
+#: templates/stats/statuses.html:13
+msgid "Orders per location"
+msgstr ""
+
+#: templates/stats/statuses.html:14
+msgid ""
+"Shows the number of orders with a particular status at the selected location "
+"that have been assigned to a technician over the specified time period."
+msgstr ""
+
+#: templates/stats/statuses.html:17
+msgid "Orders per user"
+msgstr ""
+
+#: templates/stats/statuses.html:18
+msgid ""
+"Shows the number of orders with a particular status per each user at the "
+"given location that have been assigned to a technician over the specified "
+"time period."
+msgstr ""
+
+#: views/account.py:26
+msgid "Profile Settings"
+msgstr ""
+
+#: views/account.py:34 views/admin.py:235
+msgid "Settings saved"
+msgstr ""
+
+#: views/account.py:47
+msgid "Error in user details"
+msgstr ""
+
+#: views/account.py:73
+msgid "My Orders"
+msgstr ""
+
+#: views/account.py:81
+msgid "Sign In"
+msgstr ""
+
+#: views/account.py:102
+#, python-format
+msgid "%s logged in"
+msgstr ""
+
+#: views/account.py:109
+msgid "Incorrect username or password"
+msgstr ""
+
+#: views/account.py:111
+msgid "Login failed"
+msgstr ""
+
+#: views/account.py:119
+msgid "You have logged out"
+msgstr ""
+
+#: views/account.py:223
+msgid "Users can only delete their own calendars!"
+msgstr ""
+
+#: views/account.py:229
+msgid "Calendar deleted"
+msgstr ""
+
+#: views/account.py:232
+msgid "Really delete this calendar?"
+msgstr ""
+
+#: views/account.py:251
+msgid "Calendar saved"
+msgstr ""
+
+#: views/account.py:274
+msgid "Calendar event created"
+msgstr ""
+
+#: views/account.py:284
+msgid "Event saved"
+msgstr ""
+
+#: views/account.py:287
+msgid "Edit Event"
+msgstr ""
+
+#: views/account.py:298
+msgid "Calendar event updated"
+msgstr ""
+
+#: views/account.py:305
+msgid "Users can only delete their own events!"
+msgstr ""
+
+#: views/account.py:313
+msgid "Calendar event deleted"
+msgstr ""
+
+#: views/account.py:316
+msgid "Really delete this event?"
+msgstr ""
+
+#: views/account.py:340
+msgid "Your registration is now pending approval."
+msgstr ""
+
+#: views/account.py:354
+msgid "All notifications cleared"
+msgstr ""
+
+#: views/account.py:365 views/order.py:855
+msgid "Search query is too short"
+msgstr ""
+
+#: views/account.py:381
+msgid "Search results"
+msgstr ""
+
+#: views/admin.py:65 views/admin.py:270 views/customer.py:140
+#: views/device.py:152
+#, python-format
+msgid "%s saved"
+msgstr ""
+
+#: views/admin.py:71
+msgid "GSX account for this sold-to and environment already exists"
+msgstr ""
+
+#: views/admin.py:83
+msgid "GSX account deleted"
+msgstr ""
+
+#: views/admin.py:126
+msgid "Checklist saved"
+msgstr ""
+
+#: views/admin.py:138
+msgid "Checklist deleted"
+msgstr ""
+
+#: views/admin.py:142
+msgid "Really delete this checklist?"
+msgstr ""
+
+#: views/admin.py:143
+msgid "This will also delete all checklist values."
+msgstr ""
+
+#: views/admin.py:179
+#, python-format
+msgid "Tag %s saved"
+msgstr ""
+
+#: views/admin.py:194
+msgid "Tag deleted"
+msgstr ""
+
+#: views/admin.py:197
+msgid "Really delete this tag?"
+msgstr ""
+
+#: views/admin.py:215
+msgid "Check your settings"
+msgstr ""
+
+#: views/admin.py:283 views/admin.py:542 views/customer.py:154
+#, python-format
+msgid "%s deleted"
+msgstr ""
+
+#: views/admin.py:322
+msgid "Field saved"
+msgstr ""
+
+#: views/admin.py:334
+msgid "Field deleted"
+msgstr ""
+
+#: views/admin.py:337
+msgid "Really delete this field?"
+msgstr ""
+
+#: views/admin.py:367
+#, python-format
+msgid "Template %s saved"
+msgstr ""
+
+#: views/admin.py:383
+#, python-format
+msgid "Template %s deleted"
+msgstr ""
+
+#: views/admin.py:386
+msgid "Really delete this template?"
+msgstr ""
+
+#: views/admin.py:427
+msgid "Group saved"
+msgstr ""
+
+#: views/admin.py:439
+msgid "Group deleted"
+msgstr ""
+
+#: views/admin.py:454
+msgid "User deleted"
+msgstr ""
+
+#: views/admin.py:483
+#, python-format
+msgid "User %s saved"
+msgstr ""
+
+#: views/admin.py:486
+msgid "Error in user profile data"
+msgstr ""
+
+#: views/admin.py:492
+#, python-format
+msgid "%d users"
+msgstr ""
+
+#: views/admin.py:527
+#, python-format
+msgid "Location %s saved"
+msgstr ""
+
+#: views/admin.py:548
+msgid "Really delete this location?"
+msgstr ""
+
+#: views/admin.py:549
+msgid "This will not delete the orders at this location"
+msgstr ""
+
+#: views/admin.py:560
+msgid "Create, edit and delete service queues"
+msgstr ""
+
+#: views/admin.py:587
+msgid "Failed to save queue"
+msgstr ""
+
+#: views/admin.py:594
+#, python-format
+msgid "%s queue saved"
+msgstr ""
+
+#: views/admin.py:611
+msgid "Queue deleted"
+msgstr ""
+
+#: views/admin.py:613
+msgid "Cannot delete queue"
+msgstr ""
+
+#: views/admin.py:633 views/admin.py:644
+msgid "Access denied"
+msgstr ""
+
+#: views/admin.py:637
+msgid "Manage Sites"
+msgstr ""
+
+#: views/admin.py:663
+msgid "Site saved"
+msgstr ""
+
+#: views/admin.py:682
+#, python-format
+msgid "%d users imported"
+msgstr ""
+
+#: views/checkin.py:109
+msgid "Welcome"
+msgstr ""
+
+#: views/checkin.py:120
+msgid "1/3: Enter serial number, IMEI code or choose a device"
+msgstr ""
+
+#: views/checkin.py:123
+msgid "Please enable cookies in your browser"
+msgstr ""
+
+#: views/checkin.py:134 views/checkin.py:161
+msgid "Your session has expired, please try again."
+msgstr ""
+
+#: views/checkin.py:140
+msgid "The serial number you entered is not valid."
+msgstr ""
+
+#: views/checkin.py:142
+msgid ""
+"Couldn't check warranty status, please choose device type manually or try "
+"again later."
+msgstr ""
+
+#: views/checkin.py:155
+msgid "1/3: Device details"
+msgstr ""
+
+#: views/checkin.py:192
+msgid "2/3: Problem description"
+msgstr ""
+
+#: views/checkin.py:206
+msgid "3/3: Your contact details"
+msgstr ""
+
+#: views/checkin.py:234
+msgid "Order does not exist"
+msgstr ""
+
+#: views/checkin.py:244
+msgid "2/3: Troubleshooting"
+msgstr ""
+
+#: views/checkin.py:281
+msgid "1/3: Choose your device"
+msgstr ""
+
+#: views/checkin.py:284
+msgid "Apple Keyboard"
+msgstr ""
+
+#: views/checkin.py:285
+msgid "Apple Wireless Device"
+msgstr ""
+
+#: views/checkin.py:289
+#, python-format
+msgid "1/3: %s"
+msgstr ""
+
+#: views/checkin.py:320
+msgid "Repair Status"
+msgstr ""
+
+#: views/checkin.py:332
+msgid "Waiting to be processed"
+msgstr ""
+
+#: views/checkin.py:334
+#, python-format
+msgid "Order %s not found"
+msgstr ""
+
+#: views/checkin.py:359
+msgid "Your session has expired"
+msgstr ""
+
+#: views/customer.py:89
+msgid "Customer added"
+msgstr ""
+
+#: views/customer.py:105
+msgid "Customer not found"
+msgstr ""
+
+#: views/customer.py:206
+msgid "Customer saved"
+msgstr ""
+
+#: views/customer.py:232
+msgid "Customer deleted"
+msgstr ""
+
+#: views/customer.py:250
+#, python-format
+msgid "Merge %s with"
+msgstr ""
+
+#: views/customer.py:265
+msgid "Customer records merged succesfully"
+msgstr ""
+
+#: views/customer.py:281
+#, python-format
+msgid "Customer %s moved to top level"
+msgstr ""
+
+#: views/customer.py:285
+#, python-format
+msgid "Customer %(customer)s moved to %(target)s"
+msgstr ""
+
+#: views/customer.py:307 views/device.py:282 views/order.py:875
+#: views/product.py:325 views/search.py:35
+#, python-format
+msgid "Search results for \"%s\""
+msgstr ""
+
+#: views/customer.py:357
+msgid "Search for customers"
+msgstr ""
+
+#: views/customer.py:376
+msgid "Please specify search query first"
+msgstr ""
+
+#: views/customer.py:419
+msgid "Invalid upload data"
+msgstr ""
+
+#: views/customer.py:439
+#, python-format
+msgid "%d customer(s) imported"
+msgstr ""
+
+#: views/device.py:114
+msgid "Device deleted"
+msgstr ""
+
+#: views/device.py:116
+msgid "Cannot delete device with GSX repairs"
+msgstr ""
+
+#: views/device.py:314
+#, python-format
+msgid "Devices matching \"%s\""
+msgstr ""
+
+#: views/device.py:333
+msgid "Device search"
+msgstr ""
+
+#: views/device.py:386
+msgid "Invalid serial number for parts lookup"
+msgstr ""
+
+#: views/device.py:389
+msgid "Error calculating prices. Please check your system settings."
+msgstr ""
+
+#: views/device.py:486
+#, python-format
+msgid "%d devices imported"
+msgstr ""
+
+#: views/device.py:499
+msgid "Warranty status updated successfully"
+msgstr ""
+
+#: views/error.py:24
+#, python-format
+msgid "Browser: %s"
+msgstr ""
+
+#: views/gsx.py:21
+#, python-format
+msgid "Part %s updated"
+msgstr ""
+
+#: views/gsx.py:56
+#, python-format
+msgid "Part %(part)s added to repair %(repair)s"
+msgstr ""
+
+#: views/gsx.py:85
+#, python-format
+msgid "Part %(part)s removed from %(repair)s"
+msgstr ""
+
+#: views/gsx.py:99
+msgid "Submitted repairs cannot be deleted"
+msgstr ""
+
+#: views/gsx.py:105
+msgid "GSX repair deleted"
+msgstr ""
+
+#: views/gsx.py:147
+msgid "Submitted repairs cannot be edited"
+msgstr ""
+
+#: views/gsx.py:150
+msgid "Please add some parts before creating repair"
+msgstr ""
+
+#: views/gsx.py:153
+msgid "Cannot create GSX repair without valid customer data"
+msgstr ""
+
+#: views/gsx.py:190
+msgid "GSX repair saved"
+msgstr ""
+
+#: views/gsx.py:216
+msgid "Invalid component data"
+msgstr ""
+
+#: views/gsx.py:231
+msgid "Invalid customer info"
+msgstr ""
+
+#: views/gsx.py:296
+#, python-format
+msgid "%s serial numbers updated"
+msgstr ""
+
+#: views/invoices.py:91
+#, python-format
+msgid "Receipt #%d"
+msgstr ""
+
+#: views/invoices.py:99
+#, python-format
+msgid "Invoice %s"
+msgstr ""
+
+#: views/invoices.py:110
+#, python-format
+msgid "Dispatch Order %s"
+msgstr ""
+
+#: views/invoices.py:135
+msgid "Walk-In Customer"
+msgstr ""
+
+#: views/note.py:170
+#, python-format
+msgid "Re: %s"
+msgstr ""
+
+#: views/note.py:237
+msgid "Note deleted"
+msgstr ""
+
+#: views/note.py:305
+msgid "OK"
+msgstr ""
+
+#: views/note.py:329
+#, python-format
+msgid "Notes containing \"%s\""
+msgstr ""
+
+#: views/note.py:373
+msgid "Message search"
+msgstr ""
+
+#: views/note.py:384
+msgid "Edit Escalation"
+msgstr ""
+
+#: views/order.py:142
+#, python-format
+msgid "%d search results"
+msgstr ""
+
+#: views/order.py:153
+#, python-format
+msgid "Order %s"
+msgstr ""
+
+#: views/order.py:212
+#, python-format
+msgid "Order %s reopened"
+msgstr ""
+
+#: views/order.py:349
+#, python-format
+msgid "Repair %s marked complete."
+msgstr ""
+
+#: views/order.py:404
+#, python-format
+msgid "Order %s deleted"
+msgstr ""
+
+#: views/order.py:408
+#, python-format
+msgid "Cannot delete order %(order)s: %(error)s"
+msgstr ""
+
+#: views/order.py:418
+msgid "Follow"
+msgstr ""
+
+#: views/order.py:424
+msgid "Unfollow"
+msgstr ""
+
+#: views/order.py:461
+msgid "Closed orders cannot be updated"
+msgstr ""
+
+#: views/order.py:475
+#, python-format
+msgid "User %s does not exist"
+msgstr ""
+
+#: views/order.py:503
+#, python-format
+msgid "Label %s does not exist"
+msgstr ""
+
+#: views/order.py:508 views/order.py:514 views/order.py:523
+msgid "Order updated"
+msgstr ""
+
+#: views/order.py:520
+#, python-format
+msgid "Order moved to %s"
+msgstr ""
+
+#: views/order.py:536
+#, python-format
+msgid "Service Order #%s"
+msgstr ""
+
+#: views/order.py:633
+#, python-format
+msgid "Products of order %s reserved"
+msgstr ""
+
+#: views/order.py:674 views/product.py:271
+#, python-format
+msgid "Product %s saved"
+msgstr ""
+
+#: views/order.py:761
+msgid "Order item does not exist"
+msgstr ""
+
+#: views/order.py:842
+#, python-format
+msgid "Customer %s removed"
+msgstr ""
+
+#: views/product.py:117
+msgid "Parts database uploaded for processing"
+msgstr ""
+
+#: views/product.py:206
+#, python-format
+msgid "%d products imported"
+msgstr ""
+
+#: views/product.py:211
+msgid "Upload products"
+msgstr ""
+
+#: views/product.py:274
+msgid "Error in inventory details"
+msgstr ""
+
+#: views/product.py:276
+msgid "Error in product info"
+msgstr ""
+
+#: views/product.py:296
+msgid "Product deleted"
+msgstr ""
+
+#: views/product.py:298
+msgid "Cannot delete product"
+msgstr ""
+
+#: views/product.py:371
+#, python-format
+msgid "Category %s already exists"
+msgstr ""
+
+#: views/product.py:373
+#, python-format
+msgid "Category %s saved"
+msgstr ""
+
+#: views/product.py:389
+msgid "Category deleted"
+msgstr ""
+
+#: views/product.py:437
+msgid "Price info updated from GSX"
+msgstr ""
+
+#: views/product.py:439
+msgid "Failed to update price from GSX"
+msgstr ""
+
+#: views/purchases.py:81
+#, python-format
+msgid "Product %s removed"
+msgstr ""
+
+#: views/purchases.py:96
+#, python-format
+msgid "Purchase Order %d"
+msgstr ""
+
+#: views/purchases.py:132
+#, python-format
+msgid "Purchase Order %d saved"
+msgstr ""
+
+#: views/purchases.py:136
+#, python-format
+msgid "Purchase Order %d submitted"
+msgstr ""
+
+#: views/purchases.py:144
+#, python-format
+msgid "Purchase Order #%d"
+msgstr ""
+
+#: views/purchases.py:159
+#, python-format
+msgid "Purchase Order %s has already been submitted"
+msgstr ""
+
+#: views/purchases.py:177
+#, python-format
+msgid "Products ordered with confirmation %s"
+msgstr ""
+
+#: views/purchases.py:193
+#, python-format
+msgid "Purchase Order %s deleted"
+msgstr ""
+
+#: views/shipments.py:83
+#, python-format
+msgid "%d incoming products"
+msgstr ""
+
+#: views/shipments.py:113
+#, python-format
+msgid "%d products received"
+msgstr ""
+
+#: views/shipments.py:152
+#, python-format
+msgid "Product %s received"
+msgstr ""
+
+#: views/shipments.py:186
+msgid "Browse Bulk Returns"
+msgstr ""
+
+#: views/shipments.py:210
+msgid "View bulk return"
+msgstr ""
+
+#: views/shipments.py:223
+#, python-format
+msgid "Location %s has no Ship-To"
+msgstr ""
+
+#: views/shipments.py:246
+msgid "Bulk return saved"
+msgstr ""
+
+#: views/shipments.py:251
+#, python-format
+msgid "Bulk return %s submitted"
+msgstr ""
+
+#: views/shipments.py:266
+#, python-format
+msgid "%d parts pending return"
+msgstr ""
+
+#: views/shipments.py:279
+#, python-format
+msgid "Part %s removed from bulk return"
+msgstr ""
+
+#: views/shipments.py:296
+#, python-format
+msgid "Part %s added to return"
+msgstr ""
+
+#: views/shipments.py:322
+msgid "Return DOA Part"
+msgstr ""
+
+#: views/shipments.py:331
+msgid "This part will be converted to regular inventory"
+msgstr ""
+
+#: views/shipments.py:346
+msgid "Part updated"
+msgstr ""
+
+#: views/stats.py:241
+msgid "Time Scale"
+msgstr ""
+
+#: views/stats.py:465
+msgid "No Queue"
+msgstr ""
diff --git a/servo/locale/et_EE/LC_MESSAGES/django.mo b/servo/locale/et_EE/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e7a1e9a
--- /dev/null
+++ b/servo/locale/et_EE/LC_MESSAGES/django.mo
Binary files differ
diff --git a/servo/locale/et_EE/LC_MESSAGES/django.po b/servo/locale/et_EE/LC_MESSAGES/django.po
new file mode 100644
index 0000000..000c817
--- /dev/null
+++ b/servo/locale/et_EE/LC_MESSAGES/django.po
@@ -0,0 +1,5460 @@
+# Estonian translation for SErvo.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Filipp Lepalaan <filipp@mcare.fi>, 2013.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-08-23 10:41+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms/account.py:36 forms/account.py:70 forms/account.py:78
+#: forms/admin.py:364 forms/admin.py:407 forms/admin.py:435
+#: models/account.py:138 models/common.py:197
+#: templates/devices/get_info.html:33 templates/devices/summary.html:24
+#: templates/orders/devices.html:41
+msgid "Password"
+msgstr "Parool"
+
+#: forms/account.py:41 templates/checkin/confirmation.html:6
+#: templates/orders/repair.html:28 templates/products/receive_item.html:33
+#: templates/purchases/list_pos.html:55 templates/purchases/view_po.html:23
+#: templates/shipments/list_incoming.html:19
+msgid "Confirmation"
+msgstr "Kinnitus"
+
+#: forms/account.py:54
+msgid "Password and confirmation do not match!"
+msgstr "Parool ja kinnitus ei klapi"
+
+#: forms/account.py:61
+msgid "File size of photo is too large"
+msgstr ""
+
+#: forms/account.py:67
+msgid "First Name"
+msgstr "Eesnimi"
+
+#: forms/account.py:68
+msgid "Last Name"
+msgstr "Perekonnanimi"
+
+#: forms/account.py:69 templates/accounts/print_calendar.html:23
+#: templates/orders/print_confirmation.html:24
+#: templates/orders/print_confirmation.html:54
+#: templates/orders/print_receipt.html:12
+msgid "Email Address"
+msgstr "Email"
+
+#: forms/account.py:75 forms/admin.py:358 forms/admin.py:403
+#: forms/admin.py:431 templates/devices/get_info.html:29
+#: templates/devices/summary.html:20 templates/orders/devices.html:37
+msgid "Username"
+msgstr "Kasutajatunnus"
+
+#: forms/admin.py:43 views/product.py:173
+msgid "Unsupported file encoding"
+msgstr ""
+
+#: forms/admin.py:90
+#, fuzzy
+msgid "Group members"
+msgstr "Rühmad"
+
+#: forms/admin.py:97 templates/admin/users/form.html:9
+msgid "Permissions"
+msgstr "Õigused"
+
+#: forms/admin.py:133
+msgid "A location with that name already exists"
+msgstr ""
+
+#: forms/admin.py:192
+#, python-format
+msgid "\"%s\" cannot be used as a username"
+msgstr ""
+
+#: forms/admin.py:234
+#, python-format
+msgid "Syntax error in template: %s"
+msgstr ""
+
+#: forms/admin.py:241
+msgid "Company Name"
+msgstr "Firma nimi"
+
+#: forms/admin.py:243
+#, fuzzy
+msgid "Company Logo"
+msgstr "Firma nimi"
+
+#: forms/admin.py:245
+msgid "Company-wide logo to use in print templates"
+msgstr ""
+
+#: forms/admin.py:250
+msgid "Terms of Service"
+msgstr "Hooldustingimused"
+
+#: forms/admin.py:252
+msgid ""
+"These terms will be added to your work confirmations and public check-in "
+"site."
+msgstr ""
+
+#: forms/admin.py:258
+msgid "Autocomplete GSX repairs"
+msgstr "Sulge GSX tellimused automaatselt"
+
+#: forms/admin.py:259
+msgid "Complete the GSX repair when closing a Service Order"
+msgstr ""
+
+#: forms/admin.py:264 forms/admin.py:371
+#, fuzzy
+msgid "User Account"
+msgstr "Uus konto"
+
+#: forms/admin.py:266
+msgid "User account to use for the public check-in service"
+msgstr ""
+
+#: forms/admin.py:270 templates/admin/users/groups.html:12 views/stats.py:276
+msgid "Group"
+msgstr "Rühm"
+
+#: forms/admin.py:272
+msgid "Users to choose from in the check-in interface"
+msgstr ""
+
+#: forms/admin.py:276 models/repair.py:54
+msgid "Checklist"
+msgstr ""
+
+#: forms/admin.py:278
+msgid "Checklist to show during check-in"
+msgstr ""
+
+#: forms/admin.py:282 models/queue.py:174
+msgid "Queue"
+msgstr "Järjekord"
+
+#: forms/admin.py:284
+msgid "Orders created through the check-in interface will go into this queue"
+msgstr ""
+
+#: forms/admin.py:289
+msgid "Show timeline"
+msgstr ""
+
+#: forms/admin.py:290
+msgid "Show status timeline on public repair status page"
+msgstr ""
+
+#: forms/admin.py:295
+#, fuzzy
+msgid "Show password"
+msgstr "parool"
+
+#: forms/admin.py:296
+msgid "Make checkin device password field readable"
+msgstr ""
+
+#: forms/admin.py:301
+#, fuzzy
+msgid "Show checklist results"
+msgstr "Testimistulemused"
+
+#: forms/admin.py:302
+msgid "Show checklist results in order confirmation"
+msgstr ""
+
+#: forms/admin.py:306
+msgid "Currency"
+msgstr "Valuuta"
+
+#: forms/admin.py:319
+msgid "Default GSX account"
+msgstr "Primaarne GSX konto"
+
+#: forms/admin.py:321
+msgid "Use this GSX account before and order is assigned to a queue"
+msgstr ""
+
+#: forms/admin.py:327 models/product.py:107 models/product.py:134
+msgid "Margin %"
+msgstr "Katte %"
+
+#: forms/admin.py:328
+msgid "Default margin for new products"
+msgstr "Kate uutele toodetele"
+
+#: forms/admin.py:334 models/product.py:88
+msgid "VAT %"
+msgstr "Käibemaks"
+
+#: forms/admin.py:335
+msgid "Default VAT for new products"
+msgstr "Käibemaks uutele toodetele"
+
+#: forms/admin.py:341
+msgid "Shipping Cost"
+msgstr "Transpordikulud"
+
+#: forms/admin.py:342
+msgid "Default shipping cost for new products"
+msgstr "Transpordikulud uutelete toodetele"
+
+#: forms/admin.py:348
+#, fuzzy
+msgid "Track inventory"
+msgstr "Ladu"
+
+#: forms/admin.py:349
+msgid "Unchecking this will disable tracking product amounts in your inventory"
+msgstr ""
+
+#: forms/admin.py:353
+msgid "IMAP server"
+msgstr "IMAP server"
+
+#: forms/admin.py:368 forms/admin.py:410 forms/admin.py:445
+msgid "Use SSL"
+msgstr "Kasuta SSL-i"
+
+#: forms/admin.py:373
+msgid "User account to use when creating notes from messages"
+msgstr ""
+
+#: forms/admin.py:378
+#, fuzzy
+msgid "Default Sender"
+msgstr "Vaikimisi-staatus"
+
+#: forms/admin.py:380 models/account.py:241
+#, fuzzy
+msgid "User"
+msgstr "Kasutajad"
+
+#: forms/admin.py:381 forms/returns.py:53 models/common.py:507
+#: templates/admin/users/form.html:10 views/stats.py:265
+msgid "Location"
+msgstr "Asukoht"
+
+#: forms/admin.py:382
+#, fuzzy
+msgid "Custom..."
+msgstr "Klient"
+
+#: forms/admin.py:384
+msgid "Select the default sender address for outgoing emails"
+msgstr ""
+
+#: forms/admin.py:396
+msgid "Default subject"
+msgstr "Vaikimisi-pealkiri"
+
+#: forms/admin.py:401
+msgid "SMTP server"
+msgstr "SMTP server"
+
+#: forms/admin.py:413
+msgid "SMS Gateway"
+msgstr ""
+
+#: forms/admin.py:415
+msgid "Built-in"
+msgstr ""
+
+#: forms/admin.py:423 forms/admin.py:456 forms/checkin.py:100
+msgid "Email address"
+msgstr "Email"
+
+#: forms/admin.py:426
+msgid "URL"
+msgstr "Aadress"
+
+#: forms/admin.py:428
+msgid "SMS Server URL"
+msgstr "SMS serveri aadress"
+
+#: forms/admin.py:441
+#, fuzzy
+msgid "Sender"
+msgstr "Vaikimisi-staatus"
+
+#: forms/admin.py:451
+#, fuzzy
+msgid "Notify locations"
+msgstr "Teavitused"
+
+#: forms/admin.py:452
+msgid "Daily reports will be sent to the location's email address"
+msgstr ""
+
+#: forms/admin.py:457
+msgid "Send daily reports to this email address"
+msgstr ""
+
+#: forms/admin.py:465
+msgid "Invalid margin %"
+msgstr "Vale katte %"
+
+#: forms/checkin.py:26 models/note.py:54
+#, python-format
+msgid "%s is not a valid phone number"
+msgstr "%s ei ole kõlbulik telefoninumber"
+
+#: forms/checkin.py:31
+#, python-format
+msgid "%s is not a valid serial or IMEI number"
+msgstr "%s ei ole õige seerianumber ega IMEI kood"
+
+#: forms/checkin.py:40 forms/repairs.py:22 models/customer.py:99
+#: templates/customers/view.html:38
+msgid "Country"
+msgstr "Maa"
+
+#: forms/checkin.py:46 templates/orders/devices.html:54
+#: templates/orders/print_confirmation.html:79
+msgid "Accessories"
+msgstr "Lisaseadmed"
+
+#: forms/checkin.py:48
+msgid ""
+"Please list here any accessories you'd like to check in with your device "
+"(cables, power adapters, bags, etc)"
+msgstr ""
+"Palun loetle siia seadmega kaasas olevad lisaseadmed (kaablid, laadijad, "
+"kotid jms)"
+
+#: forms/checkin.py:53
+msgid "Proof of Purchase"
+msgstr "Ostutšekk"
+
+#: forms/checkin.py:54
+msgid "Proof of Purchase is required when setting purchase date manually"
+msgstr "Ostutšekk on eriti vajalik siis kui täidad seadme ostukuupäeva käsitsi"
+
+#: forms/checkin.py:93 forms/repairs.py:15
+msgid "First name"
+msgstr "Eesnimi"
+
+#: forms/checkin.py:94 forms/repairs.py:16
+msgid "Last name"
+msgstr "Perekonnanimi"
+
+#: forms/checkin.py:97
+msgid "Company (optional)"
+msgstr "Firma"
+
+#: forms/checkin.py:104
+msgid "Phone number"
+msgstr "Telefoninumber"
+
+#: forms/checkin.py:107 forms/repairs.py:19 models/invoices.py:75
+#: templates/customers/find.html:51 templates/customers/view.html:30
+#: templates/orders/print_confirmation.html:57
+#: templates/orders/print_receipt.html:15
+msgid "Address"
+msgstr "Aadress"
+
+#: forms/checkin.py:108 forms/repairs.py:21 templates/customers/find.html:53
+msgid "City"
+msgstr "Linn"
+
+#: forms/checkin.py:109 templates/customers/find.html:52
+msgid "Postal Code"
+msgstr "Postiindeks"
+
+#: forms/checkin.py:112
+msgid "Check-in location"
+msgstr "Vastuvõtt"
+
+#: forms/checkin.py:115
+msgid "Choose where you want to leave the device"
+msgstr "Valige kuidas tahate oma seadet hooldusesse toimetada"
+
+#: forms/checkin.py:119
+msgid "Check-out location"
+msgstr "Tagastus"
+
+#: forms/checkin.py:122
+msgid "Choose where you want to pick up the device"
+msgstr "Valige kuidas te soovite oma seadet tagasi saada"
+
+#: forms/checkin.py:124
+msgid ""
+"I agree to the <a href=\"/checkin/terms/\" target=\"_blank\">terms of "
+"service.</a>"
+msgstr ""
+"Aktsepteerin <a href=\"/checkin/terms/\" target=\"_blank\">hooldustingimused."
+"</a>"
+
+#: forms/checkin.py:139
+msgid "Serial number or IMEI"
+msgstr "Seerianumber või IMEI kood"
+
+#: forms/checkin.py:146 templates/notes/view_note.html:21
+#: templates/products/receive_item.html:17
+#: templates/repairs/get_details.html:10
+#: templates/search/results/gsx_repair_details.html:6
+#: templates/shipments/list_incoming.html:18
+msgid "Service Order"
+msgstr "Hooldustellimus"
+
+#: forms/checkin.py:147
+msgid "Invalid Service Order number"
+msgstr "Vale hooldustellimuse number"
+
+#: forms/checkin.py:155 templates/checkin/issue.html:7
+msgid "Problem description"
+msgstr "Veakirjeldus"
+
+#: forms/checkin.py:160 templates/orders/repair.html:23
+msgid "Attachment"
+msgstr "Liide"
+
+#: forms/checkin.py:161
+msgid "Please use this to attach relevant documents"
+msgstr "Palun lisa siia vajalikud dokumendid"
+
+#: forms/checkin.py:166 models/common.py:457
+#: templates/accounts/print_calendar.html:45
+#: templates/accounts/view_calendar.html:32 templates/customers/view.html:42
+#: templates/devices/get_info.html:41
+#: templates/devices/search_gsx_warranty.html:48
+#: templates/devices/summary.html:28 templates/notes/list_notes.html:74
+#: templates/orders/devices.html:49 templates/orders/repair.html:32
+#: templates/repairs/get_details.html:16 templates/search/spotlight.html:27
+#: templates/search/results/gsx_repair_details.html:12
+msgid "Notes"
+msgstr "Märkmed"
+
+#: forms/checkin.py:168
+msgid "Will not appear on the print-out"
+msgstr ""
+
+#: forms/customer.py:35
+msgid "Enter a valid phone number"
+msgstr "Sisesta korrektne telefoninumber"
+
+#: forms/customer.py:44 forms/product.py:17
+msgid "Name contains"
+msgstr "Nimi sisaldab"
+
+#: forms/customer.py:48
+msgid "Email contains"
+msgstr "Email sisaldab"
+
+#: forms/customer.py:52
+msgid "Address contains"
+msgstr "Aadress sisaldab"
+
+#: forms/customer.py:56
+#, fuzzy
+msgid "Checked in between"
+msgstr "Tellimus loodud"
+
+#: forms/customer.py:72
+msgid "Please specify at least one parameter"
+msgstr ""
+
+#: forms/customer.py:76
+#, fuzzy
+msgid "CSV file"
+msgstr "fail"
+
+#: forms/customer.py:80
+msgid "Skip duplicates"
+msgstr ""
+
+#: forms/customer.py:81
+msgid "Skip customers with existing email addresses"
+msgstr ""
+
+#: forms/customer.py:87
+msgid "Data file should be in text format"
+msgstr ""
+
+#: forms/devices.py:26 forms/orders.py:97
+#, fuzzy
+msgid "Created between"
+msgstr "Tellimus loodud"
+
+#: forms/devices.py:34
+#, fuzzy
+msgid "Serial number contains"
+msgstr "Saatja sisaldab"
+
+#: forms/devices.py:42 forms/product.py:25
+#, fuzzy
+msgid "Description contains"
+msgstr "Saaja sisaldab"
+
+#: forms/devices.py:61
+#, fuzzy
+msgid "Serial numbers may not include spaces"
+msgstr "Saatja sisaldab"
+
+#: forms/invoices.py:39 forms/orders.py:57 forms/product.py:125
+msgid "State is"
+msgstr "Seis on"
+
+#: forms/invoices.py:41 forms/invoices.py:50 forms/product.py:127
+msgid "Any"
+msgstr ""
+
+#: forms/invoices.py:42 forms/product.py:128 models/order.py:128
+msgid "Open"
+msgstr "Ava"
+
+#: forms/invoices.py:43
+msgid "Paid"
+msgstr "Makstud"
+
+#: forms/invoices.py:49
+#, fuzzy
+msgid "Payment method is"
+msgstr "Maksumeetod"
+
+#: forms/invoices.py:55
+#, fuzzy
+msgid "Status is not"
+msgstr "Staatused"
+
+#: forms/invoices.py:61 forms/invoices.py:64 forms/product.py:140
+#: forms/product.py:143 forms/product.py:173 forms/product.py:187
+#: views/stats.py:250
+msgid "Start date"
+msgstr "Alates"
+
+#: forms/invoices.py:69 forms/invoices.py:72 forms/product.py:148
+#: forms/product.py:151 forms/product.py:180 forms/product.py:194
+#: views/stats.py:255
+msgid "End date"
+msgstr "Kuni"
+
+#: forms/invoices.py:77
+#, fuzzy
+msgid "Customer name contains"
+msgstr "Kliendi nimi"
+
+#: forms/invoices.py:81 forms/notes.py:43
+msgid "Service Order is"
+msgstr "Hooldustellimus on"
+
+#: forms/notes.py:29 models/note.py:81 templates/notes/view_note.html:19
+msgid "From"
+msgstr "Saatja"
+
+#: forms/notes.py:40
+msgid "Body contains"
+msgstr "Tekst sisaldab"
+
+#: forms/notes.py:41
+msgid "Recipient contains"
+msgstr "Saaja sisaldab"
+
+#: forms/notes.py:42
+msgid "Sender contains"
+msgstr "Saatja sisaldab"
+
+#: forms/orders.py:52 forms/product.py:165
+msgid "Location is"
+msgstr "Asukoht on"
+
+#: forms/orders.py:62
+msgid "Queue is"
+msgstr "Järjekord"
+
+#: forms/orders.py:67 models/queue.py:236 templates/admin/queues/form.html:27
+#: templates/devices/search_gsx_repairs.html:9
+#: templates/invoices/index.html:47 templates/orders/list.html:14
+#: templates/repairs/get_details.html:20
+#: templates/search/results/gsx_repair_details.html:16 views/stats.py:286
+msgid "Status"
+msgstr "Staatus"
+
+#: forms/orders.py:72
+#, fuzzy
+msgid "Created by"
+msgstr "Loodud"
+
+#: forms/orders.py:77 templates/orders/list.html:13
+msgid "Assigned to"
+msgstr "Käsitleja"
+
+#: forms/orders.py:82 templates/orders/edit.html:91
+msgid "Label"
+msgstr ""
+
+#: forms/orders.py:87 models/common.py:345
+msgid "Green"
+msgstr "Roheline"
+
+#: forms/orders.py:88
+msgid "Yellow"
+msgstr "Kollane"
+
+#: forms/orders.py:89 models/common.py:347
+msgid "Red"
+msgstr "Punane"
+
+#: forms/orders.py:90
+msgid "Grey"
+msgstr "Hall"
+
+#: forms/orders.py:92
+msgid "Color"
+msgstr "Värv"
+
+#: forms/product.py:21
+#, fuzzy
+msgid "Code contains"
+msgstr "Tekst sisaldab"
+
+#: forms/product.py:29
+#, fuzzy
+msgid "Device model is"
+msgstr "seadmed"
+
+#: forms/product.py:40
+msgid "Product datafile"
+msgstr "Tootefail"
+
+#: forms/product.py:48
+msgid "Parts database file"
+msgstr ""
+
+#: forms/product.py:52
+msgid "Import vintage parts"
+msgstr ""
+
+#: forms/product.py:57
+msgid "Update product prices"
+msgstr "Uuenda toodete hindu"
+
+#: forms/product.py:112
+#, python-format
+msgid "Product code %s contains invalid characters"
+msgstr ""
+
+#: forms/product.py:129
+msgid "Submitted"
+msgstr "Saadetud"
+
+#: forms/product.py:130 templates/products/receive_item.html:26
+#: templates/products/view.html:158 templates/purchases/list_pos.html:56
+msgid "Received"
+msgstr "Saabunud"
+
+#: forms/product.py:156
+#, fuzzy
+msgid "Reference contains"
+msgstr "Saatja sisaldab"
+
+#: forms/product.py:170
+#, fuzzy
+msgid "Ordered between"
+msgstr "Tellimus loodud"
+
+#: forms/product.py:184
+#, fuzzy
+msgid "Received between"
+msgstr "Vastu võtnud"
+
+#: forms/product.py:198
+#, fuzzy
+msgid "Confirmation is"
+msgstr "Kinnitus"
+
+#: forms/product.py:201
+#, fuzzy
+msgid "Service order is"
+msgstr "Hooldustellimus on"
+
+#: forms/repairs.py:17 models/invoices.py:69 templates/customers/find.html:49
+#: templates/customers/view.html:22
+msgid "Email"
+msgstr "Email"
+
+#: forms/repairs.py:18 models/invoices.py:63
+#: templates/accounts/print_calendar.html:24 templates/customers/find.html:50
+#: templates/customers/search.html:18 templates/customers/view.html:26
+#: templates/orders/print_confirmation.html:25
+#: templates/search/results/customers.html:14
+msgid "Phone"
+msgstr "Telefon"
+
+#: forms/repairs.py:20 models/common.py:413 models/customer.py:88
+msgid "ZIP Code"
+msgstr "Postiindeks"
+
+#: forms/repairs.py:23 models/common.py:325 models/product.py:224
+msgid "Other"
+msgstr "Muu"
+
+#: forms/repairs.py:58 models/repair.py:103
+msgid "Technician"
+msgstr ""
+
+#: forms/repairs.py:73
+msgid "Replacement serial number must be set"
+msgstr ""
+
+#: forms/returns.py:16
+msgid "Reason"
+msgstr "Põhjus"
+
+#: forms/returns.py:18 forms/returns.py:29 forms/returns.py:39
+msgid "Select..."
+msgstr "Vali..."
+
+#: forms/returns.py:19
+msgid "Part not needed"
+msgstr "Varuosa ei olnud vaja"
+
+#: forms/returns.py:20
+msgid "Duplicated part"
+msgstr "Topelt varuosa"
+
+#: forms/returns.py:21
+msgid "Added wrong part"
+msgstr "Lisatud vale varuosa"
+
+#: forms/returns.py:22
+msgid "Tried to cancel order"
+msgstr ""
+
+#: forms/returns.py:23
+msgid "Customer refused order"
+msgstr "Klient keeldus hooldusest"
+
+#: forms/returns.py:27
+msgid "Type"
+msgstr "Tüüp"
+
+#: forms/returns.py:30
+msgid "Diagnostic"
+msgstr "Diagnostika"
+
+#: forms/returns.py:31
+msgid "Un-Opened"
+msgstr "Avamata"
+
+#: forms/returns.py:38 models/parts.py:46
+msgid "Symptom Code"
+msgstr "Sümptom"
+
+#: forms/returns.py:42 models/parts.py:51
+msgid "Symptom Modifier"
+msgstr ""
+
+#: management/commands/cron.py:81 models/order.py:127
+msgid "Unassigned"
+msgstr "Ilma käsitlejata"
+
+#: management/commands/cron.py:82
+msgid "No Status"
+msgstr "Ilma staatuseta"
+
+#: management/commands/cron.py:86
+#, python-format
+msgid "Repairs aging beyond limits at %s"
+msgstr ""
+
+#: management/commands/cron.py:115
+msgid "Products stocked below limit"
+msgstr ""
+
+#: messaging/sms.py:14
+#, fuzzy
+msgid "SMS sender not configured"
+msgstr "Primaarne GSX konto"
+
+#: messaging/sms.py:51
+msgid "SMS sender name not configured"
+msgstr ""
+
+#: messaging/sms.py:77
+#, python-format
+msgid "Failed to send message to %s"
+msgstr ""
+
+#: messaging/sms.py:90
+msgid "Wrong sender name"
+msgstr ""
+
+#: messaging/sms.py:91
+msgid "Too many messages in one request"
+msgstr ""
+
+#: messaging/sms.py:92
+#, fuzzy
+msgid "Invalid username or password"
+msgstr "Vale parool või kasutajanimi"
+
+#: messaging/sms.py:93
+msgid "Insufficient credits on your account"
+msgstr ""
+
+#: messaging/sms.py:94
+msgid "Unsuccessful message submission"
+msgstr ""
+
+#: messaging/sms.py:95 messaging/sms.py:96
+msgid "Internal system error"
+msgstr ""
+
+#: messaging/sms.py:118
+#, python-format
+msgid "Unknown error (%s)"
+msgstr ""
+
+#: messaging/sms.py:132
+msgid "No SMS HTTP gateway defined"
+msgstr ""
+
+#: models/account.py:53 templates/admin/users/index.html:7
+msgid "New User"
+msgstr "Uus kasutaja"
+
+#: models/account.py:64 templates/orders/edit.html:33
+msgid "Current Location"
+msgstr "Praegune asukoht"
+
+#: models/account.py:65
+msgid "Orders you create will be registered to this location."
+msgstr ""
+
+#: models/account.py:71
+msgid "queues"
+msgstr "järjekorrad"
+
+#: models/account.py:74
+#, fuzzy
+msgid "Danish"
+msgstr "Soome"
+
+#: models/account.py:75
+msgid "Dutch"
+msgstr ""
+
+#: models/account.py:76
+msgid "English"
+msgstr "Inglise"
+
+#: models/account.py:77
+msgid "Estonian"
+msgstr "Eesti"
+
+#: models/account.py:78
+msgid "Finnish"
+msgstr "Soome"
+
+#: models/account.py:79
+msgid "Swedish"
+msgstr "Rootsi"
+
+#: models/account.py:85
+msgid "language"
+msgstr "keel"
+
+#: models/account.py:86
+msgid "Select which language you want to use Servo in."
+msgstr ""
+
+#: models/account.py:94 models/common.py:428
+#, fuzzy
+msgid "Time zone"
+msgstr "Ajatsoon"
+
+#: models/account.py:98
+msgid "Denmark"
+msgstr "Taani"
+
+#: models/account.py:99
+msgid "Estonia"
+msgstr "Eesti"
+
+#: models/account.py:100
+msgid "Finland"
+msgstr "Soome"
+
+#: models/account.py:101
+msgid "United States"
+msgstr "Ãœhendriigid"
+
+#: models/account.py:102
+msgid "Netherlands"
+msgstr ""
+
+#: models/account.py:103
+msgid "Sweden"
+msgstr "Rootsi"
+
+#: models/account.py:109
+msgid "region"
+msgstr "Regioon"
+
+#: models/account.py:110
+msgid "Affects formatting of numbers, dates and currencies."
+msgstr "Mõjutab numbrite, kuupäevade ja summade kuvamist."
+
+#: models/account.py:114
+msgid "email notifications"
+msgstr "saada teated mailiga"
+
+#: models/account.py:115
+msgid "Event notifications will also be emailed to you."
+msgstr ""
+
+#: models/account.py:119
+msgid "print automatically"
+msgstr ""
+
+#: models/account.py:120
+msgid "Opens print dialog automatically."
+msgstr ""
+
+#: models/account.py:126
+msgid "tech ID"
+msgstr "Tech ID"
+
+#: models/account.py:132 models/common.py:190
+msgid "User ID"
+msgstr "Kasutaja ID"
+
+#: models/account.py:144
+msgid "PO prefix"
+msgstr ""
+
+#: models/account.py:145
+msgid "GSX repairs you create will be prefixed"
+msgstr ""
+
+#: models/account.py:152 models/customer.py:107 models/device.py:152
+#: models/product.py:202
+msgid "photo"
+msgstr "foto"
+
+#: models/account.py:153
+msgid "Maximum avatar size is 1MB"
+msgstr ""
+
+#: models/account.py:242 templates/admin/index.html:24 views/admin.py:405
+msgid "Users & Groups"
+msgstr "Kasutajad ja rühmad"
+
+#: models/calendar.py:25 models/common.py:632 models/common.py:709
+#: models/order.py:704 models/repair.py:32
+msgid "title"
+msgstr "nimi"
+
+#: models/calendar.py:26 templates/accounts/calendars.html:7
+msgid "New Calendar"
+msgstr "Uus kalender"
+
+#: models/calendar.py:31
+msgid "hours per day"
+msgstr "tundi päevas"
+
+#: models/calendar.py:32
+msgid "How many hours per day should be in this calendar"
+msgstr "Kui mitu tundi peaks selles kalendris olema keskmiselt iga päev"
+
+#: models/calendar.py:49
+#, python-format
+msgid ""
+"%(hours)s hours total in %(workdays)s days (%(overtime)s hours overtime)."
+msgstr ""
+
+#: models/calendar.py:52
+#, python-format
+msgid "%(hours)s hours total in %(workdays)s days."
+msgstr ""
+
+#: models/common.py:176
+msgid "New GSX Account"
+msgstr "Uus GSX konto"
+
+#: models/common.py:177 models/queue.py:126
+msgid "Sold-To"
+msgstr ""
+
+#: models/common.py:178 models/common.py:444
+msgid "Ship-To"
+msgstr ""
+
+#: models/common.py:183
+msgid "Region"
+msgstr "Regioon"
+
+#: models/common.py:202
+msgid "Environment"
+msgstr "Keskkond"
+
+#: models/common.py:227
+#, fuzzy
+msgid "Default GSX account not configured"
+msgstr "Primaarne GSX konto"
+
+#: models/common.py:252
+#, fuzzy
+msgid "Configuration error"
+msgstr "Konfiguratsioon"
+
+#: models/common.py:297 models/escalations.py:25
+#, fuzzy
+msgid "GSX Account"
+msgstr "GSX kontod"
+
+#: models/common.py:298 templates/admin/index.html:12
+msgid "GSX Accounts"
+msgstr "GSX kontod"
+
+#: models/common.py:317 templates/admin/tags/index.html:6
+msgid "New Tag"
+msgstr "Uus tag"
+
+#: models/common.py:318 models/common.py:393 models/customer.py:25
+#: models/customer.py:59 models/queue.py:197
+msgid "name"
+msgstr "nimi"
+
+#: models/common.py:322 templates/devices/index.html:49
+#: templates/devices/search_gsx.html:8 templates/devices/search_gsx.html:10
+#: templates/devices/search_gsx_error.html:8 templates/devices/view.html:7
+#: templates/search/results/gsx.html:8 templates/search/results/gsx.html:10
+msgid "Device"
+msgstr "Seade"
+
+#: models/common.py:323 models/common.py:625
+#: templates/accounts/updates.html:37 templates/invoices/index.html:44
+#: templates/notes/form.html:109 templates/orders/dispatch.html:105
+#: templates/orders/edit.html:114 templates/orders/gsx_repair_form.html:89
+#: templates/orders/list.html:10 templates/orders/parts.html:33
+#: templates/orders/tabs.html:4 templates/products/index_outgoing.html:25
+#: templates/products/view.html:129 templates/products/view.html.py:155
+#: templates/products/view.html:180 templates/purchases/list_pos.html:57
+#: templates/shipments/list_returns.html:14
+msgid "Order"
+msgstr "Tellimus"
+
+#: models/common.py:324 templates/notes/form.html:37
+msgid "Note"
+msgstr "Märge"
+
+#: models/common.py:331 models/common.py:639
+msgid "type"
+msgstr "tüüp"
+
+#: models/common.py:344
+msgid "Default"
+msgstr ""
+
+#: models/common.py:346
+msgid "Orange"
+msgstr "Oranž"
+
+#: models/common.py:348
+msgid "Blue"
+msgstr "Sinine"
+
+#: models/common.py:374 templates/orders/notes.html:32
+msgid "Tag"
+msgstr "Tägi"
+
+#: models/common.py:375 templates/admin/index.html:30
+#: templates/devices/form.html:25 templates/products/form.html:51
+msgid "Tags"
+msgstr "Tägid"
+
+#: models/common.py:394 templates/admin/locations/index.html:5
+msgid "New Location"
+msgstr "Uus asukoht"
+
+#: models/common.py:400 models/customer.py:71
+msgid "phone"
+msgstr "Telefon"
+
+#: models/common.py:402 models/customer.py:76
+msgid "email"
+msgstr "email"
+
+#: models/common.py:407 models/customer.py:82
+msgid "address"
+msgstr "aadress"
+
+#: models/common.py:419 models/customer.py:94
+msgid "city"
+msgstr "linn"
+
+#: models/common.py:437
+msgid "Accounts"
+msgstr "Kontod"
+
+#: models/common.py:450
+msgid "Timezone"
+msgstr "Ajatsoon"
+
+#: models/common.py:458
+msgid "Will be shown on print templates"
+msgstr ""
+
+#: models/common.py:465
+msgid "Logo"
+msgstr ""
+
+#: models/common.py:470
+msgid "Enabled"
+msgstr ""
+
+#: models/common.py:508 templates/admin/index.html:15
+#: templates/stats/index.html:9 templates/stats/locations.html:6
+#: templates/stats/queues.html:6 templates/stats/sales.html:6
+#: templates/stats/statuses.html:6 views/admin.py:510
+msgid "Locations"
+msgstr "Asukohad"
+
+#: models/common.py:624 templates/customers/view.html:7
+#: templates/devices/search_gsx_repairs.html:8
+#: templates/invoices/index.html:45 templates/orders/dispatch.html:20
+#: templates/orders/gsx_repair_form.html:44 templates/orders/list.html:11
+#: templates/products/view.html:130
+msgid "Customer"
+msgstr "Klient"
+
+#: models/common.py:626 templates/orders/dispatch.html:63
+#: templates/orders/print_confirmation.html:105
+#: templates/orders/print_dispatch.html:13 templates/orders/products.html:10
+#: templates/products/list.html:7 templates/products/view.html:43
+#: templates/purchases/edit_po.html:32 templates/purchases/view_po.html:31
+msgid "Product"
+msgstr "Toode"
+
+#: models/common.py:631 templates/admin/fields/index.html:6
+msgid "New Field"
+msgstr "Uus väli"
+
+#: models/common.py:645
+msgid "format"
+msgstr "formaat"
+
+#: models/common.py:647
+msgid "value"
+msgstr "väärtus"
+
+#: models/common.py:664
+#, fuzzy
+msgid "Field"
+msgstr "Väljad"
+
+#: models/common.py:665 templates/admin/index.html:27
+msgid "Fields"
+msgstr "Väljad"
+
+#: models/common.py:710 templates/admin/templates/list_templates.html:6
+msgid "New Template"
+msgstr "Uus põhi"
+
+#: models/common.py:713
+msgid "content"
+msgstr "sisu"
+
+#: models/common.py:732
+#, fuzzy
+msgid "Template"
+msgstr "Põhjad"
+
+#: models/common.py:733 templates/admin/index.html:33
+#: templates/admin/queues/form.html:13
+msgid "Templates"
+msgstr "Põhjad"
+
+#: models/common.py:741
+msgid "file"
+msgstr "fail"
+
+#: models/common.py:761
+#, fuzzy, python-format
+msgid "%s is not of an allowed file type"
+msgstr "%s ei ole lubatud failitüüp"
+
+#: models/customer.py:24 templates/admin/users/index.html:12
+#: templates/customers/index.html:16
+msgid "New Group"
+msgstr "Uus rühm"
+
+#: models/customer.py:54 models/customer.py:142
+msgid "company"
+msgstr "firma"
+
+#: models/customer.py:60 templates/customers/choose.html:17
+#: templates/customers/index.html:7 templates/customers/search.html:5
+#: templates/search/spotlight.html:6
+msgid "New Customer"
+msgstr "Uus klient"
+
+#: models/customer.py:114 templates/admin/users/tabs.html:6
+#: templates/customers/index.html:55
+msgid "Groups"
+msgstr "Rühmad"
+
+#: models/customer.py:121
+msgid "tags"
+msgstr "tagid"
+
+#: models/customer.py:128 models/device.py:146
+msgid "notes"
+msgstr "märkmed"
+
+#: models/customer.py:136
+msgid "devices"
+msgstr "seadmed"
+
+#: models/device.py:39 templates/devices/find.html:27
+#: templates/devices/get_info.html:14
+#: templates/devices/search_gsx_warranty.html:15
+#: templates/devices/summary.html:13 templates/orders/devices.html:20
+#: templates/orders/print_confirmation.html:72
+#: templates/orders/products.html:28 templates/products/index_outgoing.html:28
+#: templates/search/results/gsx_warranty.html:15
+#: templates/shipments/list_returns.html:17
+msgid "Serial Number"
+msgstr "Seerianumber"
+
+#: models/device.py:43 templates/devices/choose.html:17
+#: templates/devices/index.html:7 templates/devices/index.html.py:10
+msgid "New Device"
+msgstr "Uus seade"
+
+#: models/device.py:44 models/order.py:708 models/queue.py:44
+#: models/queue.py:202
+msgid "description"
+msgstr "kirjeldus"
+
+#: models/device.py:49
+msgid "Apple"
+msgstr ""
+
+#: models/device.py:50 models/product.py:183
+msgid "Brand"
+msgstr "Tootja"
+
+#: models/device.py:57 templates/orders/print_confirmation.html:68
+msgid "IMEI Number"
+msgstr "IMEI number"
+
+#: models/device.py:63 templates/devices/search_gsx_warranty.html:24
+#: templates/orders/devices.html:31
+msgid "Initial Activation Policy"
+msgstr "Aktiveerimisproofil"
+
+#: models/device.py:69 templates/devices/search_gsx_warranty.html:22
+#: templates/orders/devices.html:29
+msgid "Applied Activation Policy"
+msgstr "Aktiveerimisproofil"
+
+#: models/device.py:79 templates/devices/index.html:26
+msgid "Product Line"
+msgstr "Tooteseeria"
+
+#: models/device.py:85
+msgid "Products that are compatible with this device instance"
+msgstr ""
+
+#: models/device.py:92
+msgid "configuration"
+msgstr "konfiguratsioon"
+
+#: models/device.py:96 models/parts.py:78
+msgid "Apple Limited Warranty"
+msgstr "Apple aastane garantii"
+
+#: models/device.py:97 models/parts.py:82
+msgid "AppleCare Protection Plan"
+msgstr "AppleCare Protection Plan"
+
+#: models/device.py:98
+msgid "Customer Satisfaction (CS) Code"
+msgstr ""
+
+#: models/device.py:99 models/parts.py:75
+msgid "Custom Bid Contracts"
+msgstr ""
+
+#: models/device.py:100
+msgid "3'rd Party Warranty"
+msgstr "Kolmanda osapoole garantii"
+
+#: models/device.py:101
+msgid "Out Of Warranty (No Coverage)"
+msgstr "Ilma garantiita"
+
+#: models/device.py:102 views/gsx.py:131
+msgid "Unknown"
+msgstr "Tundmatu"
+
+#: models/device.py:109 templates/checkin/device.html:16
+#: templates/devices/find.html:30 templates/devices/get_info.html:10
+#: templates/devices/search_gsx_warranty.html:11
+#: templates/devices/summary.html:9 templates/orders/devices.html:16
+#: templates/orders/print_confirmation.html:65
+#: templates/search/results/gsx_warranty.html:11
+msgid "Warranty Status"
+msgstr "Garantiistaatus"
+
+#: models/device.py:115
+msgid "username"
+msgstr "kasutaja"
+
+#: models/device.py:121
+msgid "password"
+msgstr "parool"
+
+#: models/device.py:126
+msgid "Date Purchased"
+msgstr "Ostetud"
+
+#: models/device.py:134
+msgid "Purchase Country"
+msgstr "Ostupaik"
+
+#: models/device.py:158
+msgid "Image URL"
+msgstr "Pildi URL"
+
+#: models/device.py:163
+msgid "Manual URL"
+msgstr "Manuaali URL"
+
+#: models/device.py:168 templates/devices/view.html:38
+#: templates/devices/view.html.py:40
+msgid "Exploded View"
+msgstr ""
+
+#: models/device.py:285
+#, python-format
+msgid "Invalid argument for warranty check: %s"
+msgstr ""
+
+#: models/device.py:352
+#, fuzzy, python-format
+msgid "SLA Group: %s"
+msgstr "Rühmad"
+
+#: models/device.py:354
+msgid "This unit is eligible for Onsite Service."
+msgstr ""
+
+#: models/device.py:356
+msgid "Parts and Labor are covered."
+msgstr ""
+
+#: models/escalations.py:96
+#, fuzzy, python-format
+msgid "Escalation %s"
+msgstr "Uus asukoht"
+
+#: models/invoices.py:27 models/invoices.py:163
+msgid "No Charge"
+msgstr "Ilma maksuta"
+
+#: models/invoices.py:28 models/invoices.py:164
+msgid "Cash"
+msgstr "Sularaha"
+
+#: models/invoices.py:29 models/invoices.py:165
+#: templates/products/view.html:179
+msgid "Invoice"
+msgstr "Arve"
+
+#: models/invoices.py:30 models/invoices.py:166
+msgid "Credit Card"
+msgstr "Krediitkaart"
+
+#: models/invoices.py:31 models/invoices.py:167
+msgid "Mail payment"
+msgstr ""
+
+#: models/invoices.py:32 models/invoices.py:168
+msgid "Online payment"
+msgstr "Online-makse"
+
+#: models/invoices.py:39 models/invoices.py:173
+#: templates/invoices/index.html:50
+msgid "Payment Method"
+msgstr "Maksumeetod"
+
+#: models/invoices.py:42
+msgid "paid"
+msgstr "on makstud"
+
+#: models/invoices.py:56
+msgid "Walk-in"
+msgstr ""
+
+#: models/invoices.py:57 templates/customers/find.html:48
+#: templates/orders/print_confirmation.html:177
+msgid "Name"
+msgstr "Nimi"
+
+#: models/invoices.py:81 models/repair.py:127
+#: templates/devices/search_gsx_repairs.html:6 templates/orders/repair.html:30
+#: templates/products/receive_item.html:23
+#: templates/products/receive_item.html:30 templates/purchases/edit_po.html:33
+#: templates/purchases/list_pos.html:54 templates/purchases/view_po.html:21
+#: templates/purchases/view_po.html:32
+#: templates/shipments/edit_bulk_return.html:57
+msgid "Reference"
+msgstr "Viide"
+
+#: models/invoices.py:132
+msgid "Sales Price"
+msgstr "Müügihind"
+
+#: models/invoices.py:186 views/invoices.py:159
+#, python-format
+msgid "Order %s dispatched"
+msgstr ""
+
+#: models/invoices.py:195
+#, python-format
+msgid "Payment for %0.2f received"
+msgstr ""
+
+#: models/note.py:66
+msgid "subject"
+msgstr "pealkiri"
+
+#: models/note.py:70
+msgid "Message"
+msgstr "Sõnum"
+
+#: models/note.py:87
+msgid "To"
+msgstr ""
+
+#: models/note.py:112 models/order.py:763
+msgid "report"
+msgstr "raporteeri"
+
+#: models/note.py:116
+msgid "read"
+msgstr "loetud"
+
+#: models/note.py:121
+msgid "flagged"
+msgstr ""
+
+#: models/note.py:159
+#, fuzzy, python-format
+msgid "Default Address <%s>"
+msgstr "Email"
+
+#: models/note.py:271
+msgid "As Unreported"
+msgstr ""
+
+#: models/note.py:271
+msgid "As Reported"
+msgstr ""
+
+#: models/note.py:274
+msgid "As Unread"
+msgstr ""
+
+#: models/note.py:274
+msgid "As Read"
+msgstr ""
+
+#: models/note.py:277
+msgid "As Unflagged"
+msgstr ""
+
+#: models/note.py:277
+msgid "As Flagged"
+msgstr ""
+
+#: models/note.py:319 models/note.py:409
+#, python-format
+msgid "Already sent message to %s"
+msgstr ""
+
+#: models/note.py:364 models/note.py:448
+#, fuzzy, python-format
+msgid "Message sent to %s"
+msgstr "Sõnum saadetud"
+
+#: models/note.py:385
+msgid "System is not configured for built-in SMS support."
+msgstr ""
+
+#: models/note.py:417
+msgid "SMS gateway not configured"
+msgstr ""
+
+#: models/note.py:473
+#, fuzzy, python-format
+msgid "Escalation %s created"
+msgstr "Uus asukoht"
+
+#: models/note.py:476
+#, fuzzy, python-format
+msgid "Escalation %s updated"
+msgstr "Uus asukoht"
+
+#: models/note.py:481
+msgid "Note saved"
+msgstr "Märge salvestatud"
+
+#: models/order.py:41 models/queue.py:60
+msgid "priority"
+msgstr "prioriteet"
+
+#: models/order.py:106 models/repair.py:39
+msgid "queue"
+msgstr "järjekord"
+
+#: models/order.py:112
+msgid "status"
+msgstr "staatus"
+
+#: models/order.py:129
+#, fuzzy
+msgid "Closed"
+msgstr "Sulge"
+
+#: models/order.py:215
+#, python-format
+msgid "%s added"
+msgstr "%s lisatud"
+
+#: models/order.py:219
+msgid "This device has already been added to this order"
+msgstr ""
+
+#: models/order.py:237
+#, fuzzy, python-format
+msgid "%s removed"
+msgstr "Klient %s eemaldatud"
+
+#: models/order.py:260
+msgid "Closed a moment ago"
+msgstr ""
+
+#: models/order.py:261
+#, fuzzy, python-format
+msgid "Closed for %(time)s"
+msgstr "suletud %s"
+
+#: models/order.py:265
+#, python-format
+msgid "%s a moment ago"
+msgstr ""
+
+#: models/order.py:268
+#, fuzzy, python-format
+msgid "%(status)s for %(time)s"
+msgstr "täna kell %(time)s"
+
+#: models/order.py:272
+#, fuzzy
+msgid "Created a moment ago"
+msgstr "Loodud"
+
+#: models/order.py:273
+#, fuzzy, python-format
+msgid "Unassigned for %(delta)s"
+msgstr "Ilma käsitlejata %s"
+
+#: models/order.py:277
+#, fuzzy
+msgid "Started a moment ago"
+msgstr "Alustatud"
+
+#: models/order.py:278
+#, fuzzy, python-format
+msgid "Open for %(delta)s"
+msgstr "Töö all %s"
+
+#: models/order.py:281
+msgid "Select place"
+msgstr "Vali paik"
+
+#: models/order.py:284
+msgid "Select status"
+msgstr "Vali staatus"
+
+#: models/order.py:291
+msgid "Select user"
+msgstr "Vali kasutaja"
+
+#: models/order.py:294
+msgid "Select queue"
+msgstr "Vali järjekord"
+
+#: models/order.py:303 templates/default.html:35
+#: templates/accounts/orders.html:58 templates/accounts/tabs.html:7
+#: templates/customers/view.html:8 templates/devices/view.html:8
+#: templates/orders/index.html:49 templates/search/spotlight.html:22
+#: views/order.py:43
+msgid "Orders"
+msgstr "Tellimused"
+
+#: models/order.py:312 views/order.py:195
+#, python-format
+msgid "Order %s closed"
+msgstr "Tellimus %s suletud"
+
+#: models/order.py:411
+#, fuzzy
+msgid "Order must belong to a queue to set status"
+msgstr "Tellimus on määratud sellesse järjekorda."
+
+#: models/order.py:445
+msgid "Status unassigned"
+msgstr "Staatus eemaldatud"
+
+#: models/order.py:475
+msgid "Removed from queue"
+msgstr "Eemaldatud järjekorrast"
+
+#: models/order.py:501
+msgid "Order unassigned"
+msgstr "Kasutaja eemaldatud"
+
+#: models/order.py:505
+#, fuzzy, python-format
+msgid "Order %(order)s assigned to %(user)s"
+msgstr "Kasutaja eemaldatud"
+
+#: models/order.py:598 views/order.py:693 views/purchases.py:90
+#, python-format
+msgid "Product %s added"
+msgstr "Toode %s lisatud"
+
+#: models/order.py:604
+#, fuzzy, python-format
+msgid "Product %s removed from order"
+msgstr "Toode %s eemaldatud"
+
+#: models/order.py:685
+msgid "Can set assignee"
+msgstr "Võib muuta tellimuse käsitlejat"
+
+#: models/order.py:686
+msgid "Can change status"
+msgstr "Võib muuta staatust"
+
+#: models/order.py:687
+msgid "Can follow order"
+msgstr "Võib jälgida tellimust"
+
+#: models/order.py:719
+msgid "amount"
+msgstr "kogus"
+
+#: models/order.py:724
+#, fuzzy
+msgid "KGB Serial Number"
+msgstr "KBB seerianumber"
+
+#: models/order.py:759
+msgid "dispatched"
+msgstr ""
+
+#: models/order.py:768
+msgid "sales price"
+msgstr "Müügihind"
+
+#: models/order.py:783
+msgid "KBB Serial Number"
+msgstr "KBB seerianumber"
+
+#: models/order.py:790 templates/orders/print_confirmation.html:120
+#: templates/orders/print_receipt.html:23
+msgid "IMEI"
+msgstr ""
+
+#: models/order.py:794 templates/orders/print_confirmation.html:106
+#: templates/orders/print_dispatch.html:14
+msgid "Warranty"
+msgstr "Garantii"
+
+#: models/order.py:795 templates/devices/parts.html:11
+#: templates/products/form.html:11 templates/products/get_info.html:23
+#: templates/products/list.html:9 templates/products/view.html:64
+msgid "Exchange Price"
+msgstr "Vahetushind"
+
+#: models/order.py:796 templates/devices/parts.html:10
+#: templates/products/form.html:12 templates/products/get_info.html:21
+#: templates/products/list.html:8 templates/products/view.html:62
+msgid "Stock Price"
+msgstr "Müügihind"
+
+#: models/order.py:803
+#, fuzzy
+msgid "Price category"
+msgstr "Uus kategooria"
+
+#: models/order.py:810
+#, fuzzy
+msgid "symptom code"
+msgstr "CS-kood"
+
+#: models/order.py:816
+#, fuzzy
+msgid "symptom modifier"
+msgstr "CS-kood"
+
+#: models/order.py:1005 models/product.py:589
+#, fuzzy
+msgid "New Shipping Method"
+msgstr "Transpordikulud"
+
+#: models/order.py:1034 views/order.py:238
+#, python-format
+msgid "Order %s created"
+msgstr "Tellimus %s loodud"
+
+#: models/parts.py:76
+#, fuzzy
+msgid "Customer Satisfaction"
+msgstr "Kliendi andmed"
+
+#: models/parts.py:77
+#, fuzzy
+msgid "DOA Coverage"
+msgstr "Garantii"
+
+#: models/parts.py:79
+msgid "Missing Upon First Use"
+msgstr ""
+
+#: models/parts.py:80
+#, fuzzy
+msgid "Out of Warranty (no coverage)"
+msgstr "Ilma garantiita"
+
+#: models/parts.py:81
+#, fuzzy
+msgid "AppleCare Parts Agreement"
+msgstr "AppleCare Protection Plan"
+
+#: models/parts.py:83
+msgid "Quality Program"
+msgstr ""
+
+#: models/parts.py:84
+msgid "AppleCare Repair Agreement"
+msgstr ""
+
+#: models/parts.py:85 templates/orders/devices.html:13
+#, fuzzy
+msgid "Repeat Service"
+msgstr "Teenus"
+
+#: models/parts.py:86
+#, fuzzy
+msgid "Additional Part Coverage"
+msgstr "Garantii"
+
+#: models/parts.py:87
+msgid "Additional Service Coverage"
+msgstr ""
+
+#: models/parts.py:88
+#, fuzzy
+msgid "NEW - AppleCare Protection Plan"
+msgstr "AppleCare Protection Plan"
+
+#: models/parts.py:89
+#, fuzzy
+msgid "Consumer Law Coverage"
+msgstr "Garantii"
+
+#: models/parts.py:231
+#, fuzzy
+msgid "Unregister from Return"
+msgstr "peab tagastama"
+
+#: models/parts.py:233 templates/repairs/part_menu.html:8
+#, fuzzy
+msgid "Register for Return"
+msgstr "peab tagastama"
+
+#: models/parts.py:328
+msgid "GSX repair has no dispatch ID"
+msgstr ""
+
+#: models/product.py:64
+msgid "code"
+msgstr "kood"
+
+#: models/product.py:71
+msgid "Substituted (new) code of this part"
+msgstr ""
+
+#: models/product.py:76 templates/products/index.html:9
+msgid "New Product"
+msgstr "Uus toode"
+
+#: models/product.py:77 models/queue.py:27
+#: templates/products/index_outgoing.html:24
+#: templates/shipments/list_returns.html:13
+msgid "Title"
+msgstr "Nimetus"
+
+#: models/product.py:82 models/repair.py:64 templates/devices/find.html:28
+msgid "Description"
+msgstr "Kirjeldus"
+
+#: models/product.py:93
+msgid "Don't update price when recalculating prices or importing parts"
+msgstr ""
+
+#: models/product.py:100 models/product.py:128
+msgid "Purchase price"
+msgstr "Ostuhind"
+
+#: models/product.py:113 models/product.py:140
+msgid "Net price"
+msgstr "Netohind"
+
+#: models/product.py:114 models/product.py:141
+msgid "Purchase price + margin %"
+msgstr "Ostuhind + katte %"
+
+#: models/product.py:120 models/product.py:147
+msgid "Sales price"
+msgstr "Müügihind"
+
+#: models/product.py:121 models/product.py:148
+msgid "Purchase price + margin % + shipping + VAT %"
+msgstr "ostuhind + katte % + saatmiskulud + KM %"
+
+#: models/product.py:153
+msgid "is serialized"
+msgstr ""
+
+#: models/product.py:154
+msgid "Product has a serial number"
+msgstr "Tootel on seerianumber"
+
+#: models/product.py:171
+msgid "Warranty (months)"
+msgstr "Garantii (kuud)"
+
+#: models/product.py:177
+msgid "Shelf code"
+msgstr "Riiulikood"
+
+#: models/product.py:189 templates/admin/inventory/index.html:9
+#: templates/products/index.html:60 templates/products/view.html:10
+msgid "Categories"
+msgstr "Kategooriad"
+
+#: models/product.py:195
+msgid "device models"
+msgstr "seadmed"
+
+#: models/product.py:205
+msgid "shipping"
+msgstr "postikulud"
+
+#: models/product.py:213
+msgid "component group"
+msgstr ""
+
+#: models/product.py:220
+msgid "Module"
+msgstr "Moodul"
+
+#: models/product.py:221
+msgid "Replacement"
+msgstr "Vahetus"
+
+#: models/product.py:222
+msgid "Service"
+msgstr "Teenus"
+
+#: models/product.py:223
+msgid "Service Contract"
+msgstr "Teenuseleping"
+
+#: models/product.py:231
+msgid "part type"
+msgstr "tüüp"
+
+#: models/product.py:238
+msgid "EEE code"
+msgstr "EEE kood"
+
+#: models/product.py:252
+#, python-format
+msgid "%d months"
+msgstr "%d kuud"
+
+#: models/product.py:419
+#, python-format
+msgid "Product %s not found in inventory."
+msgstr "Toodet %s ei ole laos"
+
+#: models/product.py:495
+msgid "Can change product amount"
+msgstr ""
+
+#: models/product.py:507 templates/products/index.html:12
+msgid "New Category"
+msgstr "Uus kategooria"
+
+#: models/product.py:557
+msgid "minimum amount"
+msgstr "miinimumkogus"
+
+#: models/product.py:561
+msgid "reserved amount"
+msgstr "reserveeritud kogus"
+
+#: models/product.py:565
+msgid "stocked amount"
+msgstr "laos"
+
+#: models/product.py:569
+msgid "ordered amount"
+msgstr "tellitud"
+
+#: models/purchases.py:35
+msgid "The location from which this PO was created"
+msgstr ""
+
+#: models/purchases.py:42
+msgid "reference"
+msgstr "viide"
+
+#: models/purchases.py:48
+msgid "confirmation"
+msgstr "kinnitus"
+
+#: models/purchases.py:58
+msgid "supplier"
+msgstr ""
+
+#: models/purchases.py:63 models/shipments.py:65
+msgid "carrier"
+msgstr ""
+
+#: models/purchases.py:68
+msgid "tracking ID"
+msgstr ""
+
+#: models/purchases.py:73
+msgid "delivery Time"
+msgstr ""
+
+#: models/purchases.py:125
+#, python-format
+msgid "Purchase Order %d has already been submitted"
+msgstr "Ostutellimus %d on juba kinnitatud."
+
+#: models/purchases.py:178
+#, fuzzy
+msgid "Submitted orders cannot be deleted"
+msgstr "Suletud tellimusi ei või muuta"
+
+#: models/purchases.py:191 templates/products/receive_item.html:36
+#: templates/purchases/edit_po.html:35 templates/purchases/view_po.html:34
+msgid "Purchase Price"
+msgstr "Ostuhind"
+
+#: models/purchases.py:192
+#, fuzzy
+msgid "Purchase price without taxes"
+msgstr "Ostuhind"
+
+#: models/purchases.py:198
+msgid "Purchase Order"
+msgstr "Ostutellimus"
+
+#: models/purchases.py:210
+msgid "arrived"
+msgstr "on saabunud"
+
+#: models/purchases.py:248
+msgid "Product has already been received"
+msgstr "Toode %s on juba saabunud"
+
+#: models/purchases.py:291
+#, python-format
+msgid "Cannot receive item %(prod)s (%(ref)s)"
+msgstr ""
+
+#: models/purchases.py:318 views/purchases.py:215
+#, python-format
+msgid "Purchase Order %d created"
+msgstr "Ostutellimus %d loodud"
+
+#: models/queue.py:26 templates/admin/queues/index.html:5
+msgid "New Queue"
+msgstr "Uus järjekord"
+
+#: models/queue.py:33
+msgid ""
+"Orders with devices matching these keywords will be automatically assigned "
+"to this queue"
+msgstr ""
+
+#: models/queue.py:38
+msgid "locations"
+msgstr "Asukohad"
+
+#: models/queue.py:39
+msgid "Pick the locations you want this queue to appear in."
+msgstr "Vali mis asukohtades seda järjekorda kasutatakse."
+
+#: models/queue.py:52
+msgid "High"
+msgstr "Kõrge"
+
+#: models/queue.py:53
+msgid "Normal"
+msgstr "Keskmine"
+
+#: models/queue.py:54
+msgid "Low"
+msgstr "Madal"
+
+#: models/queue.py:68
+msgid "Order Created"
+msgstr "Tellimus loodud"
+
+#: models/queue.py:69
+msgid "Order has ben placed to a queue"
+msgstr "Tellimus on määratud sellesse järjekorda."
+
+#: models/queue.py:77
+msgid "Order Assigned"
+msgstr "Käsitleja määratud"
+
+#: models/queue.py:78
+msgid "Order has ben assigned to a user"
+msgstr "Tellimus on võetud töö alla."
+
+#: models/queue.py:86
+msgid "Products Ordered"
+msgstr "Tooted tellitud"
+
+#: models/queue.py:87
+msgid "Purchase Order for this Service Order has been submitted"
+msgstr "Tellimuse ostutellimus on kinnitatud."
+
+#: models/queue.py:94
+#, fuzzy
+msgid "Products Received"
+msgstr "Toode kustutatud"
+
+#: models/queue.py:95
+#, fuzzy
+msgid "Products have been received"
+msgstr "Toode %s on saabunud"
+
+#: models/queue.py:102
+#, fuzzy
+msgid "Repair Completed"
+msgstr "Valmis"
+
+#: models/queue.py:103
+#, fuzzy
+msgid "GSX repair completed"
+msgstr "GSX konto kustutatud"
+
+#: models/queue.py:111
+#, fuzzy
+msgid "Order Dispatched"
+msgstr "Tellimus %s loodud"
+
+#: models/queue.py:119
+msgid "Order Closed"
+msgstr "Tellimus suletud"
+
+#: models/queue.py:127
+msgid "GSX queries of an order in this queue will be made using this Sold-To"
+msgstr ""
+
+#: models/queue.py:134
+msgid "order template"
+msgstr "tellimuspõhi"
+
+#: models/queue.py:135
+msgid "HTML template for Service Order/Work Confirmation"
+msgstr ""
+
+#: models/queue.py:141
+#, fuzzy
+msgid "quote template"
+msgstr "tellimuspõhi"
+
+#: models/queue.py:142
+msgid "HTML template for cost estimate"
+msgstr ""
+
+#: models/queue.py:148
+msgid "receipt template"
+msgstr "tšekipõhi"
+
+#: models/queue.py:149
+msgid "HTML template for Sales Order Receipt"
+msgstr ""
+
+#: models/queue.py:155
+msgid "dispatch template"
+msgstr ""
+
+#: models/queue.py:156
+msgid "HTML template for dispatched order"
+msgstr ""
+
+#: models/queue.py:175 templates/admin/index.html:21
+#: templates/stats/index.html:10 templates/stats/locations.html:7
+#: templates/stats/queues.html:7 templates/stats/sales.html:7
+#: templates/stats/statuses.html:7 views/admin.py:577
+msgid "Queues"
+msgstr "Järjekorrad"
+
+#: models/queue.py:187
+msgid "Minutes"
+msgstr "Minutit"
+
+#: models/queue.py:188
+msgid "Hours"
+msgstr "Tundi"
+
+#: models/queue.py:189
+msgid "Days"
+msgstr "Päeva"
+
+#: models/queue.py:190
+msgid "Weeks"
+msgstr "Nädalat"
+
+#: models/queue.py:191
+msgid "Months"
+msgstr "Kuud"
+
+#: models/queue.py:196 templates/admin/statuses/index.html:5
+msgid "New Status"
+msgstr "Uus staatus"
+
+#: models/queue.py:206 models/queue.py:249
+msgid "green limit"
+msgstr "roheline piir"
+
+#: models/queue.py:210 models/queue.py:250
+msgid "yellow limit"
+msgstr "kollane piir"
+
+#: models/queue.py:215 models/queue.py:253
+msgid "time unit"
+msgstr "ajaühik"
+
+#: models/queue.py:237 templates/accounts/updates.html:28
+#: templates/admin/index.html:18 templates/admin/queues/form.html:10
+#: templates/stats/index.html:11 templates/stats/locations.html:8
+#: templates/stats/queues.html:8 templates/stats/sales.html:8
+#: templates/stats/statuses.html:8 views/admin.py:261
+msgid "Statuses"
+msgstr "Staatused"
+
+#: models/repair.py:33 templates/admin/checklist/index.html:5
+msgid "New Checklist"
+msgstr ""
+
+#: models/repair.py:42
+msgid "enabled"
+msgstr ""
+
+#: models/repair.py:55 templates/admin/index.html:36
+msgid "Checklists"
+msgstr ""
+
+#: models/repair.py:60
+msgid "Task"
+msgstr ""
+
+#: models/repair.py:107
+#, fuzzy
+msgid "Unit Received"
+msgstr "Kuupäev"
+
+#: models/repair.py:120
+msgid "Repair should be reviewed by Apple before confirmation"
+msgstr ""
+
+#: models/repair.py:135
+msgid "Notes are mandatory when requesting review."
+msgstr ""
+
+#: models/repair.py:142
+msgid "Choose files to be sent with the repair creation request"
+msgstr ""
+
+#: models/repair.py:148
+msgid "mark complete"
+msgstr "Märgi valmiks"
+
+#: models/repair.py:149
+msgid "Requires replacement serial number"
+msgstr ""
+
+#: models/repair.py:155
+#, fuzzy
+msgid "New serial number"
+msgstr "Seerianumber"
+
+#: models/repair.py:156
+#, fuzzy
+msgid "Serial Number of replacement part"
+msgstr "Asendusseadme seerianumber"
+
+#: models/repair.py:175
+msgid "Unit is eligible for consumer law coverage"
+msgstr ""
+
+#: models/repair.py:223
+msgid "New GSX Repair"
+msgstr "Uus GSX tellimus"
+
+#: models/repair.py:278
+msgid "Please add some parts to the repair"
+msgstr ""
+
+#: models/repair.py:281
+msgid "Order has not been assigned to a queue"
+msgstr ""
+
+#: models/repair.py:327 views/gsx.py:193
+#, python-format
+msgid "GSX repair %s created"
+msgstr ""
+
+#: models/repair.py:582
+#, python-format
+msgid "Repair %d"
+msgstr "Remont %d"
+
+#: models/shipments.py:43 templates/shipments/list_bulk_returns.html:10
+msgid "Tracking ID"
+msgstr ""
+
+#: models/shipments.py:90
+msgid "width"
+msgstr "laius"
+
+#: models/shipments.py:96
+msgid "height"
+msgstr "kõrgus"
+
+#: models/shipments.py:102
+msgid "length"
+msgstr "kaal"
+
+#: models/shipments.py:108
+msgid "weight"
+msgstr ""
+
+#: templates/default.html:34 templates/default.html.py:66
+msgid "Homepage"
+msgstr "Koduleht"
+
+#: templates/default.html:36
+msgid "Inventory"
+msgstr "Ladu"
+
+#: templates/default.html:37 templates/customers/view.html:9
+#: templates/devices/index.html:77 templates/devices/search.html:14
+#: templates/search/spotlight.html:16 views/device.py:48
+msgid "Devices"
+msgstr "Seadmed"
+
+#: templates/default.html:38 templates/customers/index.html:82
+#: templates/customers/list.html:5 templates/customers/search.html:33
+#: templates/customers/view.html:117 templates/search/spotlight.html:13
+#: views/customer.py:27
+msgid "Customers"
+msgstr "Kliendid"
+
+#: templates/default.html:39 templates/accounts/updates.html:27
+#: templates/customers/view.html:10 templates/notes/list_notes.html:36
+#: views/note.py:54
+msgid "Messages"
+msgstr "Sõnumid"
+
+#: templates/default.html:40 templates/accounts/tabs.html:16
+#: templates/stats/index.html:58 views/stats.py:296
+msgid "Statistics"
+msgstr "Statistika"
+
+#: templates/default.html:44 templates/accounts/orders.html:44
+#: templates/accounts/stats.html:19 templates/customers/choose.html:18
+#: templates/customers/find.html:16 templates/customers/find.html.py:31
+#: templates/customers/index.html:44 templates/devices/choose.html:18
+#: templates/devices/find.html:8 templates/devices/find.html.py:21
+#: templates/devices/index.html:20 templates/invoices/index.html:31
+#: templates/notes/find.html:9 templates/notes/find.html.py:25
+#: templates/notes/list_notes.html:29 templates/orders/index.html:36
+#: templates/products/choose.html:18 templates/products/index.html:84
+#: templates/purchases/list_pos.html:37
+#: templates/snippets/filtering_form.html:12 templates/stats/index.html:30
+msgid "Search"
+msgstr "Otsing"
+
+#: templates/default.html:67 templates/accounts/calendars.html:36
+#: templates/accounts/calendars.html:54 templates/accounts/tabs.html:13
+#: templates/accounts/view_calendar.html:64 views/account.py:128
+msgid "Calendars"
+msgstr "Kalendrid"
+
+#: templates/default.html:68 templates/accounts/settings.html:11
+#: templates/admin/users/form.html:11
+msgid "Profile"
+msgstr "Profiil"
+
+#: templates/default.html:70 templates/admin/index.html:7 views/admin.py:205
+msgid "System Settings"
+msgstr "Süsteemiseaded"
+
+#: templates/default.html:73 templates/accounts/logout.html:15
+#: templates/checkin/index.html:47
+msgid "Log out"
+msgstr "Logi välja"
+
+#: templates/default.html:100
+#, fuzzy
+msgid "No new alerts"
+msgstr "Ei ole uusi teavitusi"
+
+#: templates/default.html:105
+msgid "See All Alerts"
+msgstr ""
+
+#: templates/default.html:133
+#, fuzzy
+msgid "No new messages"
+msgstr "SMS sõnumid"
+
+#: templates/default.html:139
+#, fuzzy
+msgid "Read All Messages"
+msgstr "SMS sõnumid"
+
+#: templates/default.html:168
+msgid "Terms"
+msgstr ""
+
+#: templates/error.html:12
+msgid "An Error Occurred"
+msgstr ""
+
+#: templates/error.html:27 templates/checkin/error.html:9
+msgid "Oops!"
+msgstr ""
+
+#: templates/error.html:28 templates/checkin/error.html:10
+msgid "It appears that an error has occurred."
+msgstr "Paistab, et midagi läks rikki"
+
+#: templates/error.html:28
+msgid ""
+"I've notified the developers, but I'm sure they would really appreciate if "
+"you could briefly describe what you were doing before this happened."
+msgstr ""
+
+#: templates/error.html:33 templates/orders/dispatch.html:96
+#: templates/products/form.html:74 templates/products/form.html.py:76
+#: templates/products/receive_item.html:47
+msgid "Cancel"
+msgstr "Tühista"
+
+#: templates/error.html:34 templates/accounts/register.html:11
+#: templates/checkin/confirmation.html:11 templates/checkin/customer.html:62
+#: templates/notes/edit_escalation.html:13
+#: templates/orders/gsx_repair_form.html:79
+#: templates/products/index_outgoing.html:49
+#: templates/purchases/edit_po.html:87 templates/purchases/order_stock.html:15
+#: templates/shipments/edit_bulk_return.html:96
+#: templates/shipments/list_returns.html:38
+#: templates/shipments/submit_bulk_return.html:13
+msgid "Submit"
+msgstr "Salvesta"
+
+#: templates/error.html:38
+msgid "Thanks!"
+msgstr ""
+
+#: templates/error.html:39
+msgid ""
+"Your error report has been submitted. Thanks for helping make Servo better!"
+msgstr ""
+
+#: templates/error.html:40
+#, fuzzy
+msgid "Return Home"
+msgstr "Tootel on seerianumber"
+
+#: templates/form_buttons.html:3 templates/accounts/register.html:10
+#: templates/admin/users/form.html:50 templates/checkin/choose_model.html:137
+#: templates/checkin/customer.html:61 templates/checkin/device.html:56
+#: templates/checkin/issue.html:19 templates/checkin/sn.html:14
+#: templates/checkin/status.html:14
+msgid "Back"
+msgstr "Tagasi"
+
+#: templates/form_buttons.html:4 templates/admin/settings.html:110
+#: templates/admin/checklist/form.html:28 templates/admin/fields/form.html:14
+#: templates/admin/gsx/form.html:17 templates/admin/locations/form.html:37
+#: templates/admin/notifications/index.html:36
+#: templates/admin/statuses/form.html:18 templates/admin/tags/form.html:14
+#: templates/admin/templates/form.html:18 templates/admin/users/form.html:51
+#: templates/notes/form.html:101 templates/orders/gsx_repair_form.html:79
+#: templates/products/form.html:78 templates/purchases/edit_po.html:87
+#: templates/shipments/edit_bulk_return.html:96
+msgid "Save"
+msgstr "Salvesta"
+
+#: templates/modal.html:10 templates/admin/fields/remove.html:7
+#: templates/admin/statuses/remove.html:9 templates/admin/users/remove.html:9
+msgid "This action cannot be undone."
+msgstr "Seda toimigut ei või tühistada"
+
+#: templates/modal.html:15 templates/orders/parts.html:20
+#: templates/products/get_info.html:58 templates/snippets/modal.html:12
+msgid "Done"
+msgstr "Valmis"
+
+#: templates/pagination.html:11
+msgid "Page"
+msgstr "Lehekülg"
+
+#: templates/accounts/calendars.html:9
+msgid "New Event"
+msgstr "Uus sündmus"
+
+#: templates/accounts/calendars.html:15 templates/checkin/thanks.html:13
+#: templates/orders/toolbar.html:20
+msgid "Print"
+msgstr "Prindi"
+
+#: templates/accounts/calendars.html:16 templates/customers/find.html:75
+#: templates/customers/find.html.py:77 templates/customers/view.html:85
+#: templates/customers/view.html.py:87 templates/devices/find.html:54
+#: templates/devices/list.html:27 templates/devices/view.html:60
+#: templates/generic/index.html:15 templates/orders/customer.html:20
+#: templates/orders/devices.html:119 templates/orders/products.html:67
+#: templates/products/get_info.html:56 templates/products/view.html:119
+#: templates/products/view.html.py:121 templates/purchases/list_pos.html:92
+#: templates/purchases/list_pos.html:94
+msgid "Edit"
+msgstr "Muuda"
+
+#: templates/accounts/calendars.html:18 templates/accounts/calendars.html:20
+#: templates/accounts/delete_calendar.html:15
+#: templates/accounts/delete_calendar_event.html:11
+#: templates/admin/checklist/form.html:24
+#: templates/admin/checklist/form.html:26 templates/admin/fields/form.html:10
+#: templates/admin/fields/form.html:12 templates/admin/gsx/form.html:13
+#: templates/admin/gsx/form.html.py:15 templates/admin/gsx/remove.html:11
+#: templates/admin/locations/form.html:33
+#: templates/admin/locations/form.html:35 templates/admin/queues/form.html:29
+#: templates/admin/queues/remove.html:15 templates/admin/statuses/form.html:14
+#: templates/admin/statuses/form.html:16
+#: templates/admin/statuses/remove.html:15 templates/admin/tags/form.html:10
+#: templates/admin/tags/form.html.py:12 templates/admin/templates/form.html:14
+#: templates/admin/templates/form.html:16
+#: templates/admin/users/delete_group.html:9
+#: templates/admin/users/form.html:46 templates/admin/users/form.html.py:48
+#: templates/admin/users/remove.html:15
+#: templates/customers/delete_group.html:15 templates/customers/find.html:81
+#: templates/customers/find.html.py:83 templates/customers/remove.html:5
+#: templates/customers/remove.html.py:15 templates/customers/view.html:77
+#: templates/customers/view.html.py:79 templates/devices/find.html:56
+#: templates/devices/list.html:29 templates/devices/remove.html:15
+#: templates/devices/view.html:52 templates/devices/view.html.py:54
+#: templates/generic/delete.html:15 templates/generic/index.html:16
+#: templates/notes/form.html:99 templates/notes/remove.html:15
+#: templates/notes/view_note.html:34 templates/orders/delete_order.html:15
+#: templates/orders/gsx_repair_form.html:77 templates/orders/notes.html:50
+#: templates/orders/products.html:77 templates/orders/remove_product.html:15
+#: templates/products/delete_category.html:15
+#: templates/products/list_rows.html:44 templates/products/list_rows.html:46
+#: templates/products/remove.html:11 templates/products/view.html:109
+#: templates/products/view.html.py:113 templates/purchases/edit_po.html:37
+#: templates/purchases/list_pos.html:97 templates/purchases/list_pos.html:99
+#: templates/repairs/delete_part.html:10
+#: templates/repairs/delete_repair.html:14
+msgid "Delete"
+msgstr "Eemalda"
+
+#: templates/accounts/calendars.html:26 templates/customers/find.html:7
+#: templates/customers/find.html.py:9
+msgid "Download"
+msgstr "Lae alla"
+
+#: templates/accounts/calendars.html:47
+msgid "No calendar selected"
+msgstr "Vali kalender"
+
+#: templates/accounts/delete_calendar.html:9
+msgid "This will also delete all events in this calendar"
+msgstr "See kustutab ka kõik kalendri märkmed"
+
+#: templates/accounts/login.html:18
+msgid "Login"
+msgstr "Logi sisse"
+
+#: templates/accounts/logout.html:5
+#, fuzzy
+msgid "Logging out?"
+msgstr "Logi välja"
+
+#: templates/accounts/logout.html:9
+msgid "This will terminate your Servo session."
+msgstr ""
+
+#: templates/accounts/orders.html:7 templates/checkin/welcome.html:12
+#: templates/notes/view_note.html:33 templates/orders/customer.html:18
+#: templates/orders/index.html:7
+msgid "Create Order"
+msgstr "Loo tellimus"
+
+#: templates/accounts/orders.html:18 templates/devices/search.html:6
+#: templates/orders/index.html:18 templates/orders/parts.html:19
+#: templates/products/index.html:49 templates/products/search.html:5
+#: templates/shipments/index.html:12
+msgid "Filter results"
+msgstr "Filtreeri tulemusi"
+
+#: templates/accounts/orders.html:43 templates/orders/index.html:35
+#, fuzzy
+msgid "Reset"
+msgstr "Reserveeri"
+
+#: templates/accounts/print_calendar.html:29
+#: templates/accounts/print_calendar.html:79
+msgid "Employee"
+msgstr "Töötegija"
+
+#: templates/accounts/print_calendar.html:30
+#: templates/accounts/print_calendar.html:41
+#: templates/accounts/view_calendar.html:28
+#: templates/orders/print_confirmation.html:33
+#: templates/orders/print_confirmation.html:162
+#: templates/orders/print_confirmation.html:179
+msgid "Date"
+msgstr "Kuupäev"
+
+#: templates/accounts/print_calendar.html:42
+#: templates/accounts/view_calendar.html:29
+msgid "Started At"
+msgstr "Alustatud"
+
+#: templates/accounts/print_calendar.html:43
+#: templates/accounts/view_calendar.html:30
+msgid "Finished At"
+msgstr "Lõpetatud"
+
+#: templates/accounts/print_calendar.html:44
+#: templates/accounts/view_calendar.html:31
+msgid "Duration"
+msgstr "Kestvus"
+
+#: templates/accounts/print_calendar.html:58
+#: templates/accounts/view_calendar.html:57
+msgid "No events found"
+msgstr "Ei leitud ühtegi sündmust"
+
+#: templates/accounts/print_calendar.html:63 templates/invoices/index.html:48
+#: templates/notes/form.html:25 templates/orders/dispatch.html:67
+#: templates/orders/dispatch.html.py:83
+#: templates/orders/gsx_repair_form.html:34
+#: templates/orders/list_products.html:20
+#: templates/orders/print_confirmation.html:111
+#: templates/orders/print_confirmation.html:139
+#: templates/orders/products.html:14 templates/purchases/edit_po.html:67
+#: templates/purchases/list_pos.html:59
+msgid "Total"
+msgstr "Kokku"
+
+#: templates/accounts/print_calendar.html:75
+msgid "Manager"
+msgstr "Ãœlemus"
+
+#: templates/accounts/register.html:6 views/account.py:326
+msgid "Register"
+msgstr "Registreeru"
+
+#: templates/accounts/settings.html:12 templates/admin/locations/form.html:7
+#: templates/admin/queues/form.html:12 templates/admin/users/form.html:12
+#: templates/products/form.html:15 templates/search/spotlight.html:19
+msgid "GSX"
+msgstr "GSX"
+
+#: templates/accounts/settings.html:39 templates/accounts/tabs.html:19
+#: templates/admin/index.html:9
+msgid "Settings"
+msgstr "Seaded"
+
+#: templates/accounts/stats.html:28
+msgid "Runrate"
+msgstr ""
+
+#: templates/accounts/stats.html:29
+msgid ""
+"Shows you how many you've been assigned to during the selected period, "
+"averaged over the time scale."
+msgstr ""
+
+#: templates/accounts/tabs.html:10 templates/accounts/updates.html:26
+#: templates/accounts/updates.html.py:64 views/account.py:401
+#, fuzzy
+msgid "Updates"
+msgstr "Uuenda toodete hinnad"
+
+#: templates/accounts/updates.html:7
+msgid "Clear all"
+msgstr ""
+
+#: templates/accounts/updates.html:36
+#, fuzzy
+msgid "Event"
+msgstr "Sündmused"
+
+#: templates/accounts/view_calendar.html:10 views/stats.py:243
+msgid "Day"
+msgstr "Päev"
+
+#: templates/accounts/view_calendar.html:11 views/stats.py:244
+msgid "Week"
+msgstr "Nädal"
+
+#: templates/accounts/view_calendar.html:12 views/stats.py:245
+msgid "Month"
+msgstr "Kuu"
+
+#: templates/accounts/view_calendar.html:19
+msgid "Today"
+msgstr "Täna"
+
+#: templates/admin/index.html:46
+msgid "Systen Settings"
+msgstr "Süsteemi seaded"
+
+#: templates/admin/settings.html:11 templates/admin/checklist/form.html:9
+#: templates/admin/gsx/form.html:6 templates/admin/locations/form.html:6
+#: templates/admin/queues/form.html:9 templates/admin/statuses/form.html:6
+#: templates/admin/tags/index.html:31 templates/admin/templates/form.html:6
+#: templates/admin/users/form.html:8 templates/customers/form.html:8
+#: templates/devices/form.html:8 templates/products/form.html:10
+msgid "General"
+msgstr "Ãœldised"
+
+#: templates/admin/settings.html:12 templates/admin/inventory/index.html:8
+#: templates/products/form.html:13
+msgid "Stock"
+msgstr ""
+
+#: templates/admin/settings.html:13
+msgid "Outgoing Mail"
+msgstr ""
+
+#: templates/admin/settings.html:14
+msgid "Incoming Mail"
+msgstr "Saabuv post"
+
+#: templates/admin/settings.html:15
+msgid "Text Messages"
+msgstr "SMS sõnumid"
+
+#: templates/admin/settings.html:16
+#, fuzzy
+msgid "Check-in"
+msgstr "Praegune asukoht"
+
+#: templates/admin/settings.html:35
+msgid "Update prices"
+msgstr "Uuenda toodete hinnad"
+
+#: templates/admin/settings.html:37
+msgid "Applies the new settings to existing products after saving"
+msgstr ""
+
+#: templates/admin/settings.html:52 views/admin.py:622
+msgid "Notifications"
+msgstr "Teavitused"
+
+#: templates/admin/settings.html:67
+msgid "Sending SMS messages will be handled by Servo"
+msgstr ""
+
+#: templates/admin/settings.html:98
+#, fuzzy
+msgid "Shipping Methods"
+msgstr "Transpordikulud"
+
+#: templates/admin/settings.html:105
+msgid "Add Method"
+msgstr ""
+
+#: templates/admin/checklist/form.html:10
+msgid "Tasks"
+msgstr ""
+
+#: templates/admin/fields/remove.html:6
+msgid "Are you sure you want to delete this field?"
+msgstr ""
+
+#: templates/admin/gsx/index.html:5
+msgid "New Account"
+msgstr "Uus konto"
+
+#: templates/admin/gsx/remove.html:5
+msgid "Remove GSX account"
+msgstr "Eemalda GSX konto"
+
+#: templates/admin/notifications/index.html:6
+msgid "Reports"
+msgstr "Raportid"
+
+#: templates/admin/notifications/index.html:15
+msgid "Daily"
+msgstr "Päevane"
+
+#: templates/admin/notifications/index.html:16
+msgid "Weekly"
+msgstr "Nädalane"
+
+#: templates/admin/notifications/index.html:17
+msgid "Monthly"
+msgstr "Kuine"
+
+#: templates/admin/notifications/index.html:22
+msgid "Repairs aging beyond limits"
+msgstr ""
+
+#: templates/admin/notifications/index.html:28
+msgid "Products below stocking limit"
+msgstr ""
+
+#: templates/admin/queues/form.html:11
+#, fuzzy
+msgid "Defaults"
+msgstr "Vaikimisi-staatus"
+
+#: templates/admin/queues/form.html:28
+msgid "Time limits"
+msgstr "Ajapiirangud"
+
+#: templates/admin/queues/form.html:46
+#, fuzzy
+msgid "Add Status"
+msgstr "Staatus"
+
+#: templates/admin/queues/remove.html:5
+#, python-format
+msgid "Delete queue \"%(title)s\"?"
+msgstr ""
+
+#: templates/admin/queues/remove.html:9
+msgid "Orders in this queue will not be deleted."
+msgstr ""
+
+#: templates/admin/sites/index.html:6 views/admin.py:648
+#, fuzzy
+msgid "New Site"
+msgstr "Uus väli"
+
+#: templates/admin/sites/index.html:12
+msgid "Sites"
+msgstr ""
+
+#: templates/admin/sites/index.html:20
+#, fuzzy
+msgid "No site selected"
+msgstr "Märge salvestatud"
+
+#: templates/admin/statuses/remove.html:5
+#, python-format
+msgid "Delete status \"%(title)s\"?"
+msgstr ""
+
+#: templates/admin/tags/index.html:25
+#, fuzzy, python-format
+msgid "No %(objects)s found."
+msgstr "Ei leitud ühtegi toodet"
+
+#: templates/admin/users/delete_group.html:4
+msgid "Really delete this group?"
+msgstr ""
+
+#: templates/admin/users/delete_group.html:5
+msgid "Group member accounts will not be deleted."
+msgstr ""
+
+#: templates/admin/users/groups.html:13
+msgid "Members"
+msgstr "Liikmed"
+
+#: templates/admin/users/groups.html:31
+msgid "No groups defined"
+msgstr "Mitte ühtegi rühma"
+
+#: templates/admin/users/index.html:13 views/admin.py:675
+#, fuzzy
+msgid "Upload Users"
+msgstr "Saada seadmed"
+
+#: templates/admin/users/remove.html:5
+#, fuzzy, python-format
+msgid "Delete user %(user)s?"
+msgstr "Kustuta kasutaja "
+
+#: templates/admin/users/tabs.html:5
+msgid "Users"
+msgstr "Kasutajad"
+
+#: templates/checkin/choose_model.html:6
+msgid "Choose your device"
+msgstr "Vali oma seade"
+
+#: templates/checkin/choose_model.html:7
+msgid "Please choose your device model"
+msgstr "Palun vali oma seadme mudel"
+
+#: templates/checkin/choose_model.html:14
+msgid "MacBook Air"
+msgstr ""
+
+#: templates/checkin/choose_model.html:20
+msgid "MacBook"
+msgstr ""
+
+#: templates/checkin/choose_model.html:26
+msgid "MacBook Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:32
+msgid "iMac"
+msgstr ""
+
+#: templates/checkin/choose_model.html:40
+#, fuzzy
+msgid "iPhone"
+msgstr "Telefon"
+
+#: templates/checkin/choose_model.html:46
+msgid "iPad"
+msgstr ""
+
+#: templates/checkin/choose_model.html:52
+msgid "Apple TV"
+msgstr ""
+
+#: templates/checkin/choose_model.html:60
+msgid "Mac Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:66
+msgid "Display"
+msgstr ""
+
+#: templates/checkin/choose_model.html:72
+msgid "Mac mini"
+msgstr ""
+
+#: templates/checkin/choose_model.html:78
+#, fuzzy
+msgid "Power Mac"
+msgstr "Vooluadapter"
+
+#: templates/checkin/choose_model.html:86
+msgid "iPod Touch"
+msgstr ""
+
+#: templates/checkin/choose_model.html:92
+msgid "iPod nano"
+msgstr ""
+
+#: templates/checkin/choose_model.html:98
+msgid "iPod Shuffle"
+msgstr ""
+
+#: templates/checkin/choose_model.html:104
+msgid "iPod Classic"
+msgstr ""
+
+#: templates/checkin/choose_model.html:112
+msgid "Keyboard or Mouse"
+msgstr "Klaviatuur või hiir"
+
+#: templates/checkin/choose_model.html:118 views/checkin.py:283
+msgid "Power Adapter"
+msgstr "Vooluadapter"
+
+#: templates/checkin/choose_model.html:124
+msgid "AirPort and Wireless"
+msgstr ""
+
+#: templates/checkin/choose_model.html:130
+msgid "Other Device"
+msgstr "Muu seade"
+
+#: templates/checkin/customer.html:7
+msgid "Your contact details"
+msgstr "Teie kontaktandmed"
+
+#: templates/checkin/customer.html:8
+msgid "Please make sure your contact details are filled out correctly."
+msgstr "Palun kontrollige, et kontaktandmed oleks täidetud korrektselt."
+
+#: templates/checkin/device.html:8
+msgid "Please provide additional details about your device here."
+msgstr "Palun täpsustage siin oma seadme andmeid."
+
+#: templates/checkin/device.html:8
+msgid ""
+"Providing a username and password helps us diagnose the problem faster, "
+"especially with software-related issues."
+msgstr ""
+"Kasutajanimi ja parool aitavad meid diagnoosida probleemi kiiremini, eriti "
+"kui tegemist on tarkvara probleemiga."
+
+#: templates/checkin/device.html:8
+msgid "For faster service time, please upload your purchase receipt."
+msgstr "Palun liitke siin seadme ostutšekk."
+
+#: templates/checkin/device.html:20
+msgid "Purchased"
+msgstr "Ostetud"
+
+#: templates/checkin/device.html:24 templates/devices/get_info.html:17
+#: templates/devices/search_gsx_warranty.html:17
+#: templates/devices/summary.html:16 templates/orders/devices.html:23
+#: templates/search/results/gsx_warranty.html:17
+msgid "Configration"
+msgstr "Konfiguratsioon"
+
+#: templates/checkin/device.html:28 templates/devices/get_info.html:23
+#: templates/devices/summary.html:34
+#: templates/search/results/gsx_warranty.html:22
+msgid "Activation Profile"
+msgstr "Aktiveerimisproofil"
+
+#: templates/checkin/device.html:30 templates/devices/get_info.html:25
+#: templates/devices/search_gsx_warranty.html:26
+#: templates/devices/summary.html:36 templates/orders/devices.html:33
+#: templates/search/results/gsx_warranty.html:24
+msgid "Unlocked"
+msgstr "Lukustamata"
+
+#: templates/checkin/device.html:34 templates/customers/form.html:9
+#: templates/devices/form.html:9 templates/devices/get_info.html:37
+#: templates/devices/search_gsx_warranty.html:36
+#: templates/orders/devices.html:45 templates/orders/gsx_repair_form.html:40
+#: templates/products/form.html:14
+msgid "Details"
+msgstr "Detailid"
+
+#: templates/checkin/device.html:38
+#: templates/devices/search_gsx_warranty.html:40
+#, fuzzy
+msgid "Onsite Coverage"
+msgstr "Garantii"
+
+#: templates/checkin/device.html:42
+#: templates/devices/search_gsx_warranty.html:44
+#, fuzzy
+msgid "Contract Coverage"
+msgstr "Garantii"
+
+#: templates/checkin/device.html:57 templates/checkin/issue.html:20
+#: templates/checkin/sn.html:16
+msgid "Continue"
+msgstr "Edasi"
+
+#: templates/checkin/error.html:4
+msgid "An error occurred"
+msgstr ""
+
+#: templates/checkin/error.html:12
+msgid "Try again"
+msgstr "Proovi uuesti"
+
+#: templates/checkin/issue.html:8
+msgid "Please describe the problem in as much detail as possible."
+msgstr "Palun kirjeldage probleeme võimalikult detailselt."
+
+#: templates/checkin/sn.html:7
+msgid "Please enter your serial number"
+msgstr "Palun sisestage oma seadme seerianumber"
+
+#: templates/checkin/sn.html:8
+msgid ""
+"You can find instructions for locating the serial number by <a href=\"http://"
+"support.apple.com/kb/ht1349\" target=\"_blank\">clicking here</a>."
+msgstr ""
+"Juhised seerianumbri leidmiseks asuvad <a href=\"http://http://support.apple."
+"com/kb/HT1349?viewlocale=et_EE\" target=\"_blank\">siin</a>."
+
+#: templates/checkin/sn.html:8
+msgid ""
+"Clicking \"Choose\" will let you pick the type of your device if you don't "
+"know the serial number."
+msgstr ""
+"Klikates \"Vali\" võid valida oma seadme juhul kui sul puudub seerianumber."
+
+#: templates/checkin/sn.html:8
+msgid "If you don't provide a serial number, the service time may be longer."
+msgstr "Seerianumbri puudumine võib pikendada hooldusaega."
+
+#: templates/checkin/sn.html:9
+msgid ""
+"When checking in an accessory, please provide the serial number of the "
+"device that the accessory is used with."
+msgstr ""
+"Palun kasutage põhiseadme seerianumbrit juhul kui tegemist on lisaseadmega."
+
+#: templates/checkin/sn.html:9
+msgid ""
+"For example the serial number of your iPhone if the headset is broken, or "
+"the serial number of your Mac if you have a problem with the mouse."
+msgstr ""
+"Näiteks iPhone seerianumbrit kui viga on kõrvaklappides või Maci "
+"seerianumbrit kui klaviatuur on rikkis."
+
+#: templates/checkin/sn.html:15 templates/orders/customer.html:12
+#: templates/orders/statuses.html:3
+msgid "Choose"
+msgstr "Vali"
+
+#: templates/checkin/status-show.html:33
+msgid "Refresh"
+msgstr "Värskenda"
+
+#: templates/checkin/status-show.html:34
+msgid "Return"
+msgstr "Tagasi"
+
+#: templates/checkin/status.html:7
+msgid "Please enter your Service Order number"
+msgstr "Palun sisestage oma hooldustellimuse number"
+
+#: templates/checkin/status.html:9
+msgid ""
+"The Service Order number is an 8-digit code printed on your work confirmation"
+msgstr "Hooldustellimus on 8-kohaline number mille leiad oma töölehelt"
+
+#: templates/checkin/terms.html:6
+#, python-format
+msgid "%(company)s terms of service"
+msgstr "%(company)s hooldustingimused"
+
+#: templates/checkin/thanks.html:7 views/checkin.py:228
+msgid "Done!"
+msgstr "Valmis!"
+
+#: templates/checkin/thanks.html:8
+msgid "Your service order has been submitted."
+msgstr "Teie hooldustellimus on edukalt vastu võetud."
+
+#: templates/checkin/thanks.html:8
+msgid "Click <strong>Print</strong> to print the confirmation."
+msgstr "Ära unusta printida töölehte!"
+
+#: templates/checkin/thanks.html:8
+#, python-format
+msgid "You will be redirected to the start page in %(seconds)s seconds."
+msgstr "Sind suunatakse tagasi alguslehele %(seconds)s sekundi pärast."
+
+#: templates/checkin/troubleshoot.html:12
+msgid "Yes"
+msgstr "Jah"
+
+#: templates/checkin/troubleshoot.html:13
+msgid "No"
+msgstr "Ei"
+
+#: templates/checkin/welcome.html:7
+#, python-format
+msgid "Welcome to %(location)s service check-in."
+msgstr "Tere tulemast %(location)s'i hoolduskeskkonda."
+
+#: templates/checkin/welcome.html:8
+msgid ""
+"From here you can <strong>create</strong> a service order or check the "
+"<strong>status</strong> of an existing order."
+msgstr ""
+"Siitkaudu võite te luua <strong>uue tellimuse</strong> või kontrollida "
+"hooldustellimuse <strong>staatust</strong>."
+
+#: templates/checkin/welcome.html:8
+msgid "Please choose an option below."
+msgstr "Palun valige"
+
+#: templates/checkin/welcome.html:11
+msgid "Check Status"
+msgstr "Kontrolli staatust"
+
+#: templates/customers/choose-list.html:6 templates/customers/find.html:91
+#: templates/customers/search.html:28
+#: templates/search/results/customers.html:24
+msgid "No customers found"
+msgstr "Ei leitud ühtegi klienti"
+
+#: templates/customers/choose.html:5
+msgid "Search for customer"
+msgstr "Otsi klienti"
+
+#: templates/customers/choose.html:11
+msgid "Name, email or phone number"
+msgstr "Nimi, email või telefon"
+
+#: templates/customers/delete_group.html:5
+msgid "Delete customer group?"
+msgstr "Kustutad kliendirühma?"
+
+#: templates/customers/delete_group.html:9
+#, fuzzy
+msgid "This action will not delete the customers in this group."
+msgstr "Toiming ei kustuta klienti kliendibaasist."
+
+#: templates/customers/find.html:15 templates/customers/index.html:43
+#: templates/devices/find.html:7 templates/devices/index.html:19
+#: templates/generic/index.html:6 templates/notes/find.html:8
+#: templates/notes/list_notes.html:28
+msgid "Browse"
+msgstr "Vali"
+
+#: templates/customers/find.html:73 templates/devices/find.html:53
+#: templates/devices/list.html:26 templates/products/get_info.html:55
+msgid "View"
+msgstr "Ava"
+
+#: templates/customers/index.html:11 templates/customers/index.html.py:13
+msgid "New Contact"
+msgstr "Uus kontakt"
+
+#: templates/customers/index.html:24 templates/devices/index.html:13
+msgid "Upload"
+msgstr "Saada"
+
+#: templates/customers/index.html:27 templates/customers/index.html.py:30
+#: views/admin.py:412
+msgid "Edit Group"
+msgstr "Muuda rühma"
+
+#: templates/customers/index.html:28 templates/customers/index.html.py:31
+msgid "Delete Group"
+msgstr "Eemalda rühm"
+
+#: templates/customers/index.html:57 templates/devices/specs.html:7
+#: templates/products/index.html:61 templates/products/view.html:11
+#: views/product.py:36 views/product.py:326
+msgid "All"
+msgstr "Kõik"
+
+#: templates/customers/index.html:74
+msgid "No customer selected"
+msgstr "Vali klient"
+
+#: templates/customers/merge.html:12 templates/customers/move.html:12
+msgid "Customer name"
+msgstr "Kliendi nimi"
+
+#: templates/customers/merge.html:18 templates/customers/move.html:19
+#: templates/orders/close.html:15 templates/orders/toolbar.html:69
+#: templates/orders/toolbar.html.py:73 templates/products/choose.html:17
+#: templates/snippets/modal.html:11
+msgid "Close"
+msgstr "Sulge"
+
+#: templates/customers/move.html:5
+#, fuzzy
+msgid "Move customer"
+msgstr "Ilma kliendita"
+
+#: templates/customers/move.html:18
+msgid "Move to top"
+msgstr "Vii peatasemele"
+
+#: templates/customers/remove.html:9
+msgid ""
+"This will also delete all customer's contacts. Customer's orders will not be "
+"deleted"
+msgstr "Toiming kustutab ka kõik kliendi kontaktid. Tellimusi ei kustutata."
+
+#: templates/customers/upload.html:5
+#, fuzzy
+msgid "Upload customer data"
+msgstr "Ilma kliendita"
+
+#: templates/customers/view.html:48
+#, fuzzy
+msgid "Contacts"
+msgstr "Uus kontakt"
+
+#: templates/customers/view.html:55
+#, fuzzy
+msgid "Use in "
+msgstr "Vii tellimusele"
+
+#: templates/customers/view.html:57 templates/devices/view.html:22
+#, fuzzy
+msgid "Use in current order"
+msgstr "Vii tellimusele"
+
+#: templates/customers/view.html:61 templates/devices/find.html:50
+#: templates/devices/list.html:23
+#: templates/devices/search_gsx_warranty.html:58
+#: templates/devices/view.html:25
+#: templates/search/results/gsx_warranty.html:33
+msgid "Create Service Order"
+msgstr "Loo hooldustellimus"
+
+#: templates/customers/view.html:69 templates/customers/view.html.py:72
+#, fuzzy
+msgid "Move Customer"
+msgstr "Uus klient"
+
+#: templates/customers/view.html:70 templates/customers/view.html.py:73
+#, fuzzy
+msgid "Merge Customer"
+msgstr "Uus klient"
+
+#: templates/customers/view.html:108 templates/notes/list_notes.html:57
+msgid "No messages found"
+msgstr "Ei leitud ühtegi märget"
+
+#: templates/devices/accessories_edit.html:4
+msgid "Manage Accessories"
+msgstr "Halda lisaseadmeid"
+
+#: templates/devices/accessories_edit.html:15
+#: templates/orders/customer.html:22 templates/orders/customer.html.py:24
+#: templates/orders/devices.html:120 templates/orders/followers.html:23
+#: templates/orders/products.html:77 templates/orders/remove_customer.html:15
+#: templates/orders/remove_device.html:15
+msgid "Remove"
+msgstr "Eemalda"
+
+#: templates/devices/accessories_edit.html:19
+msgid "No accessories added"
+msgstr "Lisaseadmeid ei ole lisatud"
+
+#: templates/devices/choose-list.html:10 templates/devices/find.html:62
+#: templates/devices/search_gsx_repairs.html:24
+#: templates/search/results/gsx_notfound.html:3
+msgid "No search results"
+msgstr "Mitte ühtegi otsingutulemust"
+
+#: templates/devices/choose.html:5
+msgid "Search for device"
+msgstr "Otsi seadet"
+
+#: templates/devices/choose.html:11
+msgid "Serial number or IMEI code"
+msgstr "Seerianumber või IMEI kood"
+
+#: templates/devices/diagnostic_results.html:4
+#, fuzzy
+msgid "Result"
+msgstr "Tulemused"
+
+#: templates/devices/diagnostic_results.html:6
+msgid "Tool"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:8
+msgid "Passed modules"
+msgstr ""
+
+#: templates/devices/diagnostics.html:4
+#, fuzzy
+msgid "Diagnostics"
+msgstr "Diagnostika"
+
+#: templates/devices/diagnostics.html:11
+msgid "Fetching diagnostics..."
+msgstr "Laen diagnostikat..."
+
+#: templates/devices/find.html:29 templates/devices/search_gsx_repairs.html:7
+#: templates/notes/view_note.html:23 templates/orders/list.html:12
+#: templates/products/view.html:132 templates/products/view.html.py:156
+#: templates/products/view.html:181 templates/purchases/list_pos.html:52
+msgid "Created"
+msgstr "Loodud"
+
+#: templates/devices/find.html:47 templates/devices/list.html:19
+#: templates/devices/view.html:20 templates/products/view.html:93
+msgid "Use in"
+msgstr "Vii tellimusele"
+
+#: templates/devices/find.html:51 templates/devices/list.html:24
+msgid "Show Service Orders"
+msgstr "Näita hooldustellimused"
+
+#: templates/devices/form.html:27 templates/products/form.html:53
+msgid "Enter tag"
+msgstr "Sisesta täg"
+
+#: templates/devices/get_info.html:12
+#: templates/devices/search_gsx_warranty.html:13
+#: templates/devices/summary.html:11 templates/orders/devices.html:18
+#: templates/search/results/gsx_warranty.html:13
+msgid "Purchase Date"
+msgstr "Ostukuupäev"
+
+#: templates/devices/index.html:8 templates/devices/index.html.py:11
+msgid "Show Parts"
+msgstr "Varuosad"
+
+#: templates/devices/index.html:36 templates/orders/print_confirmation.html:64
+msgid "Model"
+msgstr "Mudel"
+
+#: templates/devices/index.html:40
+msgid "No Product Line selected"
+msgstr "Vali kategooria"
+
+#: templates/devices/index.html:58 templates/devices/list.html:35
+#: templates/search/results/devices.html:8
+msgid "No devices found"
+msgstr "Ei leitud ühtegi seadet"
+
+#: templates/devices/index.html:68
+msgid "No device selected"
+msgstr "Vali seade"
+
+#: templates/devices/parts.html:9 templates/shipments/edit_bulk_return.html:56
+#: templates/shipments/list_incoming.html:17
+msgid "Part"
+msgstr "Varuosa"
+
+#: templates/devices/parts.html:21
+#, python-format
+msgid "Substituted to %(code)s"
+msgstr ""
+
+#: templates/devices/parts.html:27 templates/products/choose-list.html:7
+#: templates/products/list_rows.html:54 templates/products/search.html:13
+#: templates/search/results/products.html:8
+msgid "No products found"
+msgstr "Ei leitud ühtegi toodet"
+
+#: templates/devices/remove.html:5
+#, python-format
+msgid "Delete device %(sn)s\"?"
+msgstr "Kustutad seadme %(sn)s\"?"
+
+#: templates/devices/remove.html:9
+msgid "This will also remove it from all service orders."
+msgstr "See eemaldab seadme ka kõigist hooldustellimustest."
+
+#: templates/devices/search_gsx.html:13 templates/devices/search_gsx.html:15
+#: templates/devices/search_gsx_error.html:10
+#: templates/orders/gsx_repair_form.html:31
+#: templates/repairs/get_details.html:22 templates/search/results/gsx.html:13
+#: templates/search/results/gsx.html:15
+#: templates/shipments/list_bulk_returns.html:12
+msgid "Parts"
+msgstr "Varuosad"
+
+#: templates/devices/search_gsx.html:18 templates/devices/search_gsx.html:20
+#: templates/devices/search_gsx_error.html:11
+#: templates/search/results/gsx.html:18 templates/search/results/gsx.html:20
+msgid "Repairs"
+msgstr "Remondid"
+
+#: templates/devices/search_gsx_parts.html:20
+#: templates/devices/search_gsx_parts.html:22
+msgid "Create Product"
+msgstr "Loo toode"
+
+#: templates/devices/search_gsx_repairs.html:5
+msgid "Number"
+msgstr "Number"
+
+#: templates/devices/search_gsx_warranty.html:31
+#: templates/purchases/view_po.html:19
+msgid "Sales Order"
+msgstr "Müügitellimus"
+
+#: templates/devices/search_gsx_warranty.html:55
+#: templates/search/results/gsx_warranty.html:35
+msgid "Use in order"
+msgstr "Vii tellimusele"
+
+#: templates/devices/search_gsx_warranty.html:60
+#: templates/search/results/gsx_warranty.html:39
+#: templates/search/results/gsx_warranty.html:41
+msgid "Download Manual"
+msgstr "Lae hooldusmanuaal"
+
+#: templates/devices/specs.html:8
+msgid "Specs"
+msgstr ""
+
+#: templates/devices/specs.html:10
+msgid "Tagi"
+msgstr ""
+
+#: templates/devices/specs.html:17
+msgid "Luo malli"
+msgstr ""
+
+#: templates/devices/upload_devices.html:5
+msgid "Upload devices"
+msgstr "Saada seadmed"
+
+#: templates/devices/view.html:10 templates/devices/view.html.py:12
+#, fuzzy
+msgid "GSX Repairs"
+msgstr "GSX parandused"
+
+#: templates/devices/view.html:33 templates/devices/view.html.py:35
+msgid "Service Manual"
+msgstr "Hooldusjuhis"
+
+#: templates/devices/view.html:44 templates/devices/view.html.py:47
+#: templates/orders/devices.html:93
+#, fuzzy
+msgid "Update Warranty Status"
+msgstr "Garantiistaatus"
+
+#: templates/devices/view.html:45 templates/devices/view.html.py:48
+#, fuzzy
+msgid "Fetch Diagnostics"
+msgstr "Lae diagnostika"
+
+#: templates/devices/view.html:73
+msgid "Fetching repairs..."
+msgstr ""
+
+#: templates/generic/index.html:22
+msgid "Create New"
+msgstr "Loo uus"
+
+#: templates/invoices/index.html:7 templates/invoices/index.html.py:105
+#: templates/products/tabs.html:17 templates/products/view.html:46
+#: views/invoices.py:23
+msgid "Invoices"
+msgstr "Arved"
+
+#: templates/invoices/index.html:8
+msgid "Sales Invoices"
+msgstr "Arved"
+
+#: templates/invoices/index.html:9
+msgid "Apple Invoices"
+msgstr "Apple arved"
+
+#: templates/invoices/index.html:46
+msgid "Date Created"
+msgstr "Loodud"
+
+#: templates/invoices/index.html:49
+msgid "Date Paid"
+msgstr "Makstud"
+
+#: templates/invoices/index.html:71
+msgid "Print receipt"
+msgstr "Prindi kviitung"
+
+#: templates/invoices/index.html:78
+msgid "No invoices found"
+msgstr "Ei leitud ühtegi arvet"
+
+#: templates/notes/form.html:38
+msgid "Attachments"
+msgstr "Liited"
+
+#: templates/notes/form.html:39
+msgid "Labels"
+msgstr ""
+
+#: templates/notes/form.html:40
+#, fuzzy
+msgid "Escalation"
+msgstr "Uus asukoht"
+
+#: templates/notes/form.html:110
+#, fuzzy
+msgid "Edit Note"
+msgstr "Lisa märge"
+
+#: templates/notes/list_notes.html:11
+#, fuzzy
+msgid "New Message"
+msgstr "SMS sõnumid"
+
+#: templates/notes/list_notes.html:12
+#, fuzzy
+msgid "New Escalation"
+msgstr "Uus asukoht"
+
+#: templates/notes/list_notes.html:37
+msgid "Inbox"
+msgstr ""
+
+#: templates/notes/list_notes.html:38
+msgid "Flagged"
+msgstr "Märgistatud"
+
+#: templates/notes/list_notes.html:39
+msgid "Sent"
+msgstr "Saadetud"
+
+#: templates/notes/list_notes.html:40
+#, fuzzy
+msgid "Escalations"
+msgstr "Uus asukoht"
+
+#: templates/notes/list_notes.html:65
+msgid "No message selected"
+msgstr "Vali märge"
+
+#: templates/notes/messages.html:5 templates/orders/notes.html:48
+#, fuzzy
+msgid "Message Log"
+msgstr "Sõnum"
+
+#: templates/notes/messages.html:18
+#, fuzzy
+msgid "No messages to display"
+msgstr "Ei leitud ühtegi märget"
+
+#: templates/notes/remove.html:5
+msgid "Delete this note?"
+msgstr "Kustutad märkme?"
+
+#: templates/notes/remove.html:9
+msgid "This will also delete any replies to this note."
+msgstr "See kustutab ka kõik märkme vastused"
+
+#: templates/notes/search-results.html:21
+#, fuzzy
+msgid "No notes found"
+msgstr "Ei leitud ühtegi arvet"
+
+#: templates/notes/search.html:7
+msgid "Create Note"
+msgstr "Loo märge"
+
+#: templates/notes/view_note.html:24
+msgid "Subject"
+msgstr "Pealkiri"
+
+#: templates/notes/view_note.html:35 templates/orders/notes.html:21
+msgid "Reply"
+msgstr "Vasta"
+
+#: templates/orders/close.html:5
+msgid "Close order"
+msgstr "Sulge tellimus"
+
+#: templates/orders/close.html:9
+msgid "This order will no longer be editable."
+msgstr ""
+
+#: templates/orders/close_repair.html:5
+msgid "Complete the GSX repair?"
+msgstr ""
+
+#: templates/orders/close_repair.html:9
+msgid "The GSX repair will no longer be editable."
+msgstr ""
+
+#: templates/orders/close_repair.html:15
+msgid "Complete"
+msgstr ""
+
+#: templates/orders/customer.html:14 templates/orders/customer.html.py:16
+#, fuzzy
+msgid "Add contact"
+msgstr "Uus kontakt"
+
+#: templates/orders/delete_order.html:5
+#, python-format
+msgid "Really delete order %(id)s? "
+msgstr ""
+
+#: templates/orders/delete_order.html:9
+msgid ""
+"This will also delete all the order's notes. This action cannot be undone."
+msgstr ""
+
+#: templates/orders/devices.html:61 templates/orders/devices.html.py:63
+msgid "View Parts"
+msgstr "Varuosad"
+
+#: templates/orders/devices.html:63
+msgid "Assign order to a queue first"
+msgstr ""
+
+#: templates/orders/devices.html:65
+msgid "View History"
+msgstr ""
+
+#: templates/orders/devices.html:67 templates/orders/devices.html.py:69
+msgid "View Accessories"
+msgstr "Lisavarustus"
+
+#: templates/orders/devices.html:78 templates/orders/devices.html.py:80
+msgid "Create Carry-In Repair"
+msgstr ""
+
+#: templates/orders/devices.html:80
+#, fuzzy
+msgid "Device has open GSX repairs"
+msgstr "Sulge GSX tellimused automaatselt"
+
+#: templates/orders/devices.html:83 templates/orders/devices.html.py:85
+#, fuzzy
+msgid "Create Onsite Repair"
+msgstr "Uus klient"
+
+#: templates/orders/devices.html:88 templates/orders/devices.html.py:90
+#, fuzzy
+msgid "Create Whole-Unit Exchange"
+msgstr "Loodud"
+
+#: templates/orders/devices.html:94
+msgid "Get Diagnostics"
+msgstr "Lae diagnostika"
+
+#: templates/orders/devices.html:97 templates/orders/devices.html.py:99
+#: templates/orders/toolbar.html:91
+#, fuzzy
+msgid "Show Barcode"
+msgstr "Varuosad"
+
+#: templates/orders/devices.html:102
+#, fuzzy
+msgid "Show Service Manual"
+msgstr "Hooldusjuhis"
+
+#: templates/orders/devices.html:105
+msgid "Show Exploded View"
+msgstr ""
+
+#: templates/orders/devices.html:112 templates/orders/devices.html.py:114
+msgid "Report"
+msgstr "Raporteeri"
+
+#: templates/orders/devices.html:131
+msgid "Order doesn't contain any devices"
+msgstr "Tellimuses pole seadmeid"
+
+#: templates/orders/devices.html:136 templates/orders/toolbar.html:14
+msgid "Add Device"
+msgstr "Lisa seade"
+
+#: templates/orders/dispatch.html:40
+#, fuzzy
+msgid "Payment"
+msgstr "Maksumeetod"
+
+#: templates/orders/dispatch.html:58 templates/products/index.html:100
+#: templates/products/search.html:18 templates/products/tabs.html:8
+#: templates/products/view.html:25 templates/purchases/edit_po.html:21
+#: templates/purchases/list_pos.html:58 views/product.py:31
+msgid "Products"
+msgstr "Tooted"
+
+#: templates/orders/dispatch.html:64
+#: templates/orders/print_confirmation.html:108
+#: templates/orders/print_dispatch.html:16
+msgid "Qty"
+msgstr "Kogus"
+
+#: templates/orders/dispatch.html:65
+#: templates/orders/print_confirmation.html:110
+#: templates/orders/products.html:13 templates/products/view.html:131
+#: templates/products/view.html.py:157 templates/products/view.html:182
+msgid "Price"
+msgstr "Hind"
+
+#: templates/orders/dispatch.html:66
+msgid "Price w/ tax"
+msgstr "Hind koos km:ga"
+
+#: templates/orders/dispatch.html:97 templates/orders/dispatch.html.py:106
+#: templates/orders/toolbar.html:64 templates/orders/toolbar.html.py:66
+#: templates/purchases/edit_po.html:22
+msgid "Dispatch"
+msgstr ""
+
+#: templates/orders/edit.html:23
+#, fuzzy
+msgid "Checkin Location"
+msgstr "Praegune asukoht"
+
+#: templates/orders/edit.html:43
+#, fuzzy
+msgid "Checkout Location"
+msgstr "Praegune asukoht"
+
+#: templates/orders/edit.html:69
+msgid "Select Customer"
+msgstr "Vali klient"
+
+#: templates/orders/edit.html:78
+msgid "Place"
+msgstr "Asukoht"
+
+#: templates/orders/edit.html:79
+msgid "Enter place"
+msgstr "Sisesta asukoht"
+
+#: templates/orders/edit.html:86
+#, fuzzy
+msgid "No place"
+msgstr "Vali paik"
+
+#: templates/orders/edit.html:92
+msgid "Enter label"
+msgstr ""
+
+#: templates/orders/edit.html:97
+msgid "No labels"
+msgstr ""
+
+#: templates/orders/followers.html:5
+msgid "Followers"
+msgstr "Jälgijad"
+
+#: templates/orders/followers.html:6
+msgid "Enter name"
+msgstr "Kasutajanimi"
+
+#: templates/orders/followers.html:19 templates/orders/followers.html.py:21
+msgid "Make primary"
+msgstr "Määra käsitlejaks"
+
+#: templates/orders/followers.html:27
+msgid "No followers"
+msgstr "Jälgijad puuduvad"
+
+#: templates/orders/gsx_repair_form.html:32
+#, fuzzy
+msgid "Checking warranty"
+msgstr "Praegune asukoht"
+
+#: templates/orders/gsx_repair_form.html:39
+msgid "Repair"
+msgstr "Parandus"
+
+#: templates/orders/gsx_repair_form.html:42
+#, fuzzy
+msgid "Components"
+msgstr "Varuosarühm"
+
+#: templates/orders/gsx_repair_form.html:73
+#: templates/purchases/edit_po.html:85
+#: templates/shipments/edit_bulk_return.html:94
+msgid "Confirm"
+msgstr "Kinnita"
+
+#: templates/orders/list.html:22
+msgid "No customer"
+msgstr "Ilma kliendita"
+
+#: templates/orders/list.html:22
+#, fuzzy
+msgid "No description"
+msgstr "kirjeldus"
+
+#: templates/orders/list.html:29
+msgid "Nobody"
+msgstr "Mitte keegi"
+
+#: templates/orders/list.html:31
+msgid "No status"
+msgstr "Ilma staatuseta"
+
+#: templates/orders/list.html:36 templates/orders/search.html:6
+#: templates/search/results/orders.html:8
+msgid "No orders found"
+msgstr "Ei leitud ühtegi tellimust"
+
+#: templates/orders/list_products.html:12
+msgid "Click to select EEE code"
+msgstr ""
+
+#: templates/orders/list_products.html:16
+#, fuzzy
+msgid "No products added"
+msgstr "Ei leitud ühtegi toodet"
+
+#: templates/orders/notes.html:8
+msgid "Order doesn't contain any notes."
+msgstr "Tellimus ei sisalda märkmeid"
+
+#: templates/orders/notes.html:40
+msgid "Mark"
+msgstr "Märgista"
+
+#: templates/orders/notes.html:47 templates/orders/repair.html:10
+msgid "Copy"
+msgstr "Kopeeri"
+
+#: templates/orders/notes.html:65
+msgid "Note has been sent"
+msgstr ""
+
+#: templates/orders/notes.html:83 templates/orders/toolbar.html:6
+#: templates/orders/toolbar.html.py:8
+msgid "Add Note"
+msgstr "Lisa märge"
+
+#: templates/orders/parts.html:15
+#, fuzzy
+msgid "Parts and Services"
+msgstr "Varuosad ja teenused"
+
+#: templates/orders/parts.html:28
+#, fuzzy
+msgid "Fetching parts..."
+msgstr "Laen diagnostikat..."
+
+#: templates/orders/parts.html:35 templates/stats/sales.html:21
+msgid "Service Parts"
+msgstr "Varuosad"
+
+#: templates/orders/print_confirmation.html:26
+msgid "Repair Number"
+msgstr "Töö number"
+
+#: templates/orders/print_confirmation.html:37
+msgid "Work Confirmation"
+msgstr "Tööleht"
+
+#: templates/orders/print_confirmation.html:44
+msgid "Customer Information"
+msgstr "Kliendi andmed"
+
+#: templates/orders/print_confirmation.html:45
+msgid "Product Information"
+msgstr "Toote andmed"
+
+#: templates/orders/print_confirmation.html:53
+#: templates/orders/print_receipt.html:11
+msgid "Daytime Phone"
+msgstr "Telefon"
+
+#: templates/orders/print_confirmation.html:66
+msgid "Estimated Date of Purchase"
+msgstr "Ostukuupäev"
+
+#: templates/orders/print_confirmation.html:69
+#, fuzzy
+msgid "Activation Policy"
+msgstr "Aktiveerimisproofil"
+
+#: templates/orders/print_confirmation.html:89
+msgid "Work Description"
+msgstr "Töökirjeldus"
+
+#: templates/orders/print_confirmation.html:101
+#: templates/orders/print_dispatch.html:9
+msgid "Products and Services"
+msgstr "Varuosad ja teenused"
+
+#: templates/orders/print_confirmation.html:107
+#: templates/orders/print_dispatch.html:15
+#, python-format
+msgid "VAT %%"
+msgstr "KM %%"
+
+#: templates/orders/print_confirmation.html:109
+#, python-format
+msgid "0%% VAT"
+msgstr "Ilma KM:ta"
+
+#: templates/orders/print_confirmation.html:119
+#: templates/orders/print_dispatch.html:24
+#: templates/orders/print_receipt.html:23
+msgid "SN"
+msgstr "SN"
+
+#: templates/orders/print_confirmation.html:151
+msgid "Customer Pickup"
+msgstr "Vastuvõtt"
+
+#: templates/orders/print_confirmation.html:160
+#, fuzzy
+msgid "Customer Name"
+msgstr "Kliendi nimi"
+
+#: templates/orders/print_confirmation.html:161
+#: templates/orders/print_confirmation.html:178
+msgid "Signature"
+msgstr "Allkiri"
+
+#: templates/orders/print_confirmation.html:168
+msgid "Received By"
+msgstr "Vastu võtnud"
+
+#: templates/orders/print_dispatch.html:5
+#, python-format
+msgid "Dispatch #%(code)s"
+msgstr ""
+
+#: templates/orders/print_dispatch.html:25
+msgid "mon."
+msgstr "kuud"
+
+#: templates/orders/print_receipt.html:6
+#, python-format
+msgid "Receipt %(number)s"
+msgstr "Kviitung %(number)s"
+
+#: templates/orders/print_receipt.html:40
+msgid "Date of invoice"
+msgstr "Kviitungi kuupäev"
+
+#: templates/orders/print_receipt.html:41
+msgid "Payment method"
+msgstr "Maksumeetod"
+
+#: templates/orders/print_receipt.html:42
+msgid "Sales Person"
+msgstr "Teid teenindas"
+
+#: templates/orders/products.html:11 templates/purchases/edit_po.html:34
+#: templates/purchases/view_po.html:33
+msgid "Amount"
+msgstr "Kogus"
+
+#: templates/orders/products.html:12
+#, fuzzy
+msgid "Price Category"
+msgstr "Uus kategooria"
+
+#: templates/orders/products.html:43 templates/orders/products.html.py:45
+msgid "Add to Repair"
+msgstr "Lisa remondile"
+
+#: templates/orders/products.html:72 templates/orders/products.html.py:74
+msgid "Create Device"
+msgstr "Loo seade"
+
+#: templates/orders/products.html:86
+msgid "Order doesn't contain any products."
+msgstr "Tellimus ei sisalda tooteid"
+
+#: templates/orders/products.html:91 templates/orders/toolbar.html:15
+#: templates/purchases/edit_po.html:6
+msgid "Add Product"
+msgstr "Lisa toode"
+
+#: templates/orders/products.html:96
+#, fuzzy
+msgid "Order Total"
+msgstr "Telli tooted"
+
+#: templates/orders/remove_customer.html:5
+msgid "Remove this customer from the order?"
+msgstr "Eemaldame selle kliendi tellimusest?"
+
+#: templates/orders/remove_customer.html:9
+msgid "This will not delete the customer from the database."
+msgstr "Toiming ei kustuta klienti kliendibaasist."
+
+#: templates/orders/remove_device.html:5
+#, python-format
+msgid "Remove %(dev)s?"
+msgstr "Eemaldan %(dev)s?"
+
+#: templates/orders/remove_device.html:9
+msgid "This will not delete the device from the database."
+msgstr ""
+
+#: templates/orders/remove_product.html:5
+msgid "Really remove product from order?"
+msgstr "Eemaldame toote tellimusest?"
+
+#: templates/orders/remove_product.html:9
+msgid "Product will not be deleted from the warehouse."
+msgstr "Toodet ei kustutata laost."
+
+#: templates/orders/repair.html:6 templates/orders/repair.html.py:8
+msgid "Complete Repair"
+msgstr ""
+
+#: templates/orders/repair.html:16
+#, fuzzy
+msgid "Symptom"
+msgstr "Sümptom"
+
+#: templates/orders/repair.html:18
+msgid "Diagnosis"
+msgstr "Diagnoos"
+
+#: templates/orders/repair.html:20
+msgid "Request Review"
+msgstr ""
+
+#: templates/orders/reserve_products.html:5
+#, python-format
+msgid "Reserve all products in order %(id)s?"
+msgstr ""
+
+#: templates/orders/reserve_products.html:11
+msgid "Reserve"
+msgstr "Reserveeri"
+
+#: templates/orders/tabs.html:8
+msgid "Events"
+msgstr "Sündmused"
+
+#: templates/orders/toolbar.html:16
+#, fuzzy
+msgid "Choose Customer"
+msgstr "Vali klient"
+
+#: templates/orders/toolbar.html:25
+#, fuzzy
+msgid "Cost Estimate"
+msgstr "Prindi hinnakalkulatsioon"
+
+#: templates/orders/toolbar.html:27
+#, fuzzy
+msgid "Receipt"
+msgstr "Prindi kviitung"
+
+#: templates/orders/toolbar.html:28
+#, fuzzy
+msgctxt "noun"
+msgid "Dispatch"
+msgstr "Tellimus %s loodud"
+
+#: templates/orders/toolbar.html:42 templates/orders/toolbar.html.py:44
+msgid "Remove Queue"
+msgstr "Eemalda järjekorrast"
+
+#: templates/orders/toolbar.html:59
+msgid "Remove Status"
+msgstr "Eemalda staatus"
+
+#: templates/orders/toolbar.html:71
+msgid "Reopen"
+msgstr "Ava"
+
+#: templates/orders/toolbar.html:82
+#, fuzzy
+msgid "Unfollow Order"
+msgstr "Võib jälgida tellimust"
+
+#: templates/orders/toolbar.html:84
+#, fuzzy
+msgid "Follow Order"
+msgstr "Jälgijad"
+
+#: templates/orders/toolbar.html:87 templates/orders/toolbar.html.py:89
+msgid "Copy Order"
+msgstr "Kopeeri tellimus"
+
+#: templates/orders/toolbar.html:94
+msgid "Priority"
+msgstr "Prioriteet"
+
+#: templates/orders/toolbar.html:103 templates/orders/toolbar.html.py:106
+msgid "Reserve Products"
+msgstr "Reserveeri tooted"
+
+#: templates/orders/toolbar.html:104 templates/orders/toolbar.html.py:107
+msgid "Order Products"
+msgstr "Telli tooted"
+
+#: templates/orders/toolbar.html:111 templates/orders/toolbar.html.py:113
+msgid "Delete Order"
+msgstr "Kustuta tellimus"
+
+#: templates/products/category_form.html:5
+msgid "Edit Product Group"
+msgstr "Muuda tooteryhmä"
+
+#: templates/products/choose.html:5
+msgid "Search for a product"
+msgstr "Otsi toodet"
+
+#: templates/products/choose.html:11
+msgid "Code, title or category"
+msgstr "Tootekood, nimetus või kategooria"
+
+#: templates/products/delete_category.html:5
+msgid "Delete product category?"
+msgstr ""
+
+#: templates/products/delete_category.html:9
+msgid "This action will not delete the products in this category."
+msgstr ""
+
+#: templates/products/form.html:42
+msgid "Add"
+msgstr "Lisa"
+
+#: templates/products/get_info.html:6
+msgid "Product Info"
+msgstr "Toote andmed"
+
+#: templates/products/get_info.html:26 templates/products/view.html:67
+msgid "Component Group"
+msgstr "Varuosarühm"
+
+#: templates/products/get_info.html:31
+msgid "EEE Code"
+msgstr "EEE kood"
+
+#: templates/products/get_info.html:40 templates/products/view.html:82
+msgid "Stocked"
+msgstr "Laos"
+
+#: templates/products/get_info.html:42 templates/products/receive_item.html:15
+#: templates/products/view.html:84 templates/purchases/list_pos.html:53
+#: templates/shipments/list_incoming.html:20
+msgid "Ordered"
+msgstr "Tellitud"
+
+#: templates/products/get_info.html:44 templates/products/view.html:86
+msgid "Reserved"
+msgstr "Reserveeritud"
+
+#: templates/products/get_info.html:49
+#, fuzzy
+msgid "Product not in inventory"
+msgstr "Toodet %s ei ole laos"
+
+#: templates/products/index.html:15 templates/products/index.html.py:18
+msgid "New Sales Order"
+msgstr "Uus müügitellimus"
+
+#: templates/products/index.html:16 templates/products/index.html.py:19
+msgid "New Purchase Order"
+msgstr "Uus ostutellimus"
+
+#: templates/products/index.html:30 templates/products/index.html.py:33
+msgid "Edit Category"
+msgstr "Muuda kategooriat"
+
+#: templates/products/index.html:31 templates/products/index.html.py:34
+msgid "Delete Category"
+msgstr "Kustuta kategooria"
+
+#: templates/products/index.html:37
+msgid "Download Products"
+msgstr "Saabuvad tooted"
+
+#: templates/products/index.html:38
+msgid "Upload Products"
+msgstr "Saada tooted"
+
+#: templates/products/index.html:39
+msgid "Upload Parts Database"
+msgstr "Saada varuosad"
+
+#: templates/products/index_outgoing.html:10
+#: templates/products/index_outgoing.html:55
+msgid "Outgoing"
+msgstr ""
+
+#: templates/products/index_outgoing.html:11
+msgid "Shipped"
+msgstr "Saadetud"
+
+#: templates/products/index_outgoing.html:23
+#: templates/purchases/edit_po.html:31
+#: templates/shipments/list_returns.html:12
+msgid "Code"
+msgstr "Kood"
+
+#: templates/products/index_outgoing.html:26
+#: templates/shipments/edit_bulk_return.html:58
+#: templates/shipments/list_returns.html:15
+msgid "Return Order"
+msgstr ""
+
+#: templates/products/index_outgoing.html:27
+#: templates/shipments/list_returns.html:16
+msgid "Register For Return"
+msgstr ""
+
+#: templates/products/index_outgoing.html:44
+#: templates/shipments/list_returns.html:33
+msgid "No parts pending return"
+msgstr ""
+
+#: templates/products/list.html:10
+msgid "In Stock"
+msgstr "Laos"
+
+#: templates/products/list_rows.html:31
+msgid "Use in Service Order"
+msgstr "Vii hooldustellimusele"
+
+#: templates/products/list_rows.html:35 templates/products/view.html:96
+msgid "Create Sales Order"
+msgstr "Loo müügitellimus"
+
+#: templates/products/list_rows.html:36
+msgid "Create Purchase Order"
+msgstr "Loo ostutellimus"
+
+#: templates/products/list_rows.html:39
+msgid "Use in Purchase Order"
+msgstr "Vii ostutellimusele"
+
+#: templates/products/receive_item.html:21
+msgid "Stocking Order"
+msgstr "Lattu"
+
+#: templates/products/receive_item.html:49
+#: templates/shipments/list_incoming.html:52
+msgid "Receive"
+msgstr ""
+
+#: templates/products/remove.html:5
+#, python-format
+msgid "Really delete product \"%(code)s\"?"
+msgstr ""
+
+#: templates/products/tabs.html:11
+msgid "Shipments"
+msgstr "Saadetised"
+
+#: templates/products/tabs.html:14 templates/purchases/list_pos.html:131
+#: templates/purchases/view_po.html:55 views/purchases.py:28
+msgid "Purchase Orders"
+msgstr "Ostutellimused"
+
+#: templates/products/upload_gsx_parts.html:5
+msgid "Upload GSX Parts"
+msgstr ""
+
+#: templates/products/view.html:44 templates/stats/index.html:12
+#: templates/stats/locations.html:9 templates/stats/queues.html:9
+#: templates/stats/sales.html:9 templates/stats/sales.html.py:13
+#: templates/stats/statuses.html:9
+#, fuzzy
+msgid "Sales"
+msgstr "Müügihind"
+
+#: templates/products/view.html:45 templates/stats/sales.html:17
+msgid "Purchases"
+msgstr "Ostutellimused"
+
+#: templates/products/view.html:104 templates/products/view.html.py:106
+#: templates/products/view.html:111
+#, fuzzy
+msgid "Update Price"
+msgstr "Uuenda toodete hinnad"
+
+#: templates/products/view.html:133
+#, fuzzy
+msgid "Dispatched"
+msgstr "Tellimus %s loodud"
+
+#: templates/products/view.html:147
+msgid "No Sales Orders"
+msgstr "Ei leitud ühtegi müügitellimust"
+
+#: templates/products/view.html:171 templates/purchases/list_pos.html:106
+msgid "No Purchase Orders"
+msgstr "Ei leitud ühtegi ostutellimust"
+
+#: templates/products/view.html:195
+msgid "No invoices"
+msgstr "Ei leitud ühtegi arvet"
+
+#: templates/purchases/edit_po.html:61
+msgid "Order does not contain any products"
+msgstr "Tellimus ei sisalda tooteid"
+
+#: templates/purchases/list_pos.html:7
+#, fuzzy
+msgid "New"
+msgstr "Uus tag"
+
+#: templates/purchases/list_pos.html:86 templates/purchases/list_pos.html:88
+msgid "Create GSX Stocking Order"
+msgstr ""
+
+#: templates/purchases/order_stock.html:5
+msgid "Submit this stocking order?"
+msgstr ""
+
+#: templates/purchases/order_stock.html:9
+msgid "Products will be ordered from GSX."
+msgstr ""
+
+#: templates/purchases/view_po.html:15
+#, fuzzy
+msgid "Created By"
+msgstr "Loodud"
+
+#: templates/purchases/view_po.html:17
+#, fuzzy
+msgid "Created At"
+msgstr "Loodud"
+
+#: templates/purchases/view_po.html:35
+#, fuzzy
+msgid "Received At"
+msgstr "Saabunud"
+
+#: templates/repairs/add_part.html:5
+#, python-format
+msgid "Add part to repair %(repair)s?"
+msgstr ""
+
+#: templates/repairs/delete_part.html:5
+msgid "Remove this part from the GSX repair?"
+msgstr ""
+
+#: templates/repairs/delete_repair.html:5
+#, fuzzy
+msgid "Delete this GSX repair?"
+msgstr "Kustutad märkme?"
+
+#: templates/repairs/delete_repair.html:9
+msgid "Only repairs that have not been submitted can be deleted."
+msgstr ""
+
+#: templates/repairs/get_details.html:12
+#: templates/search/results/gsx_repair_details.html:8
+msgid "CS Code"
+msgstr "CS-kood"
+
+#: templates/repairs/get_details.html:14
+#: templates/search/results/gsx_repair_details.html:10
+msgid "Tracking Number"
+msgstr ""
+
+#: templates/repairs/get_details.html:18
+#: templates/search/results/gsx_repair_details.html:14
+msgid "Warranty Coverage"
+msgstr "Garantii"
+
+#: templates/repairs/part_menu.html:4 templates/repairs/part_menu.html.py:7
+msgid "Print Return Label"
+msgstr ""
+
+#: templates/repairs/part_menu.html:7
+#, fuzzy
+msgid "Part has no return order number"
+msgstr "Tootel on seerianumber"
+
+#: templates/repairs/part_menu.html:11 templates/repairs/part_menu.html:13
+#, fuzzy
+msgid "Update Serial Numbers"
+msgstr "Seerianumber"
+
+#: templates/repairs/part_menu.html:17 templates/repairs/part_menu.html:27
+msgid "Return DOA"
+msgstr ""
+
+#: templates/repairs/part_menu.html:18 templates/repairs/part_menu.html:28
+#: views/shipments.py:326
+msgid "Return Good Part"
+msgstr ""
+
+#: templates/repairs/part_menu.html:19 templates/repairs/part_menu.html:29
+#: views/shipments.py:330
+msgid "Convert to Stock"
+msgstr ""
+
+#: templates/repairs/part_menu.html:22 templates/repairs/part_menu.html:24
+#: templates/repairs/part_menu.html:31
+#, fuzzy
+msgid "Remove from Repair"
+msgstr "Eemaldatud järjekorrast"
+
+#: templates/search/spotlight.html:11
+msgid "Results"
+msgstr "Tulemused"
+
+#: templates/search/spotlight.html:25
+#, fuzzy
+msgid "Products and Parts"
+msgstr "Tooted"
+
+#: templates/search/spotlight.html:28
+msgid "Articles"
+msgstr "Artiklid"
+
+#: templates/search/spotlight.html:36
+msgid "Home"
+msgstr "Koduleht"
+
+#: templates/shipments/add_to_return-results.html:6
+#, fuzzy
+msgid "No parts found"
+msgstr "Ei leitud ühtegi toodet"
+
+#: templates/shipments/add_to_return.html:5
+msgid "Add part to return"
+msgstr ""
+
+#: templates/shipments/add_to_return.html:11
+#, fuzzy
+msgid "Return order number"
+msgstr "Tootel on seerianumber"
+
+#: templates/shipments/edit_bulk_return.html:6
+#, fuzzy
+msgid "Add Part"
+msgstr "Lisa toode"
+
+#: templates/shipments/edit_bulk_return.html:9
+#: templates/shipments/edit_bulk_return.html:11
+msgid "Verify"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:28
+#: templates/shipments/view_bulk_return.html:6
+msgid "Carrier"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:34
+#: templates/shipments/view_bulk_return.html:8
+msgid "Tracking"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:40
+#: templates/shipments/view_bulk_return.html:10
+msgid "Dimensions"
+msgstr "Mõõdud"
+
+#: templates/shipments/edit_bulk_return.html:46
+#: templates/shipments/view_bulk_return.html:12
+msgid "Weight"
+msgstr "Kaal"
+
+#: templates/shipments/edit_bulk_return.html:59
+msgid "Overpack"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:85
+#: templates/shipments/view_bulk_return.html:28
+msgid "No parts registered for this shipment"
+msgstr ""
+
+#: templates/shipments/index.html:26 templates/shipments/list_incoming.html:60
+msgid "Incoming"
+msgstr "Saabuvad"
+
+#: templates/shipments/index.html:29 templates/shipments/list_returns.html:43
+msgid "Parts Pending Return"
+msgstr ""
+
+#: templates/shipments/index.html:31
+#: templates/shipments/list_bulk_returns.html:36
+#, fuzzy
+msgid "Browse Returns"
+msgstr "peab tagastama"
+
+#: templates/shipments/list_bulk_returns.html:9
+msgid "ID"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:11
+msgid "Tracking URL"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:13
+msgid "Returned"
+msgstr ""
+
+#: templates/shipments/list_incoming.html:46
+msgid "No incoming products"
+msgstr "Mitte ühtegi saavuvad toodet"
+
+#: templates/shipments/submit_bulk_return.html:4
+#, fuzzy
+msgid "Submit the bulk return?"
+msgstr "peab tagastama"
+
+#: templates/shipments/view_bulk_return.html:15
+msgid "Open Packing List"
+msgstr ""
+
+#: templates/snippets/error_modal.html:5
+msgid "An error occured..."
+msgstr ""
+
+#: templates/stats/index.html:8 templates/stats/locations.html:5
+#: templates/stats/queues.html:5 templates/stats/sales.html:5
+#: templates/stats/statuses.html:5
+msgid "Technicians"
+msgstr ""
+
+#: templates/stats/index.html:40
+#, fuzzy
+msgid "Orders Assigned"
+msgstr "Käsitleja määratud"
+
+#: templates/stats/index.html:41
+msgid ""
+"Shows how many new orders have been assigned to each technician over the "
+"given time period."
+msgstr ""
+
+#: templates/stats/index.html:44 templates/stats/locations.html:13
+#: templates/stats/queues.html:13
+#, fuzzy
+msgid "Orders Created"
+msgstr "Tellimus loodud"
+
+#: templates/stats/index.html:45
+msgid "This graph shows how many orders are checked in by each user."
+msgstr ""
+
+#: templates/stats/index.html:48 templates/stats/locations.html:29
+#: templates/stats/queues.html:25
+#, fuzzy
+msgid "Work Distribution"
+msgstr "Töökirjeldus"
+
+#: templates/stats/index.html:49
+msgid ""
+"Shows you how the total number of service orders is distributed across the "
+"technicians at this location."
+msgstr ""
+
+#: templates/stats/locations.html:14
+msgid "Shows you how many orders are created at each location."
+msgstr ""
+
+#: templates/stats/locations.html:17 templates/stats/queues.html:17
+#, fuzzy
+msgid "Orders Closed"
+msgstr "Tellimus suletud"
+
+#: templates/stats/locations.html:18
+msgid "Shows you how many orders have been closed at each location."
+msgstr ""
+
+#: templates/stats/locations.html:21 templates/stats/queues.html:21
+#, fuzzy
+msgid "Average Turnaround"
+msgstr "Keskmine kestvus"
+
+#: templates/stats/locations.html:22
+msgid "Shows how many hours it takes to complete an order at each location."
+msgstr ""
+
+#: templates/stats/locations.html:25
+#, fuzzy
+msgid "Average Runrate"
+msgstr "Keskmine kestvus"
+
+#: templates/stats/locations.html:26
+msgid "Shows you how many orders people are working on at each location."
+msgstr ""
+
+#: templates/stats/locations.html:30
+msgid ""
+"This shows you how your overall work load is distributed across your service "
+"locations."
+msgstr ""
+
+#: templates/stats/queues.html:14
+msgid ""
+"This is your total number of orders per queue in the specified time period"
+msgstr ""
+
+#: templates/stats/queues.html:18
+msgid "Shows you how many orders have been closed in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:22
+msgid "Shows how many hours it takes to complete an order in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:26
+msgid ""
+"This shows your total ratio of orders over the time period distributed over "
+"each queue."
+msgstr ""
+
+#: templates/stats/sales.html:14
+msgid "Shows you invoice totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:18
+msgid ""
+"Shows you Purchase Order totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:22
+msgid "Shows you how many parts have been ordered for each labour tier."
+msgstr ""
+
+#: templates/stats/statuses.html:13
+msgid "Orders per location"
+msgstr "Tellimusi asukoha kohta"
+
+#: templates/stats/statuses.html:14
+msgid ""
+"Shows the number of orders with a particular status at the selected location "
+"that have been assigned to a technician over the specified time period."
+msgstr ""
+
+#: templates/stats/statuses.html:17
+msgid "Orders per user"
+msgstr "Tellimusi kasutaja kohta"
+
+#: templates/stats/statuses.html:18
+msgid ""
+"Shows the number of orders with a particular status per each user at the "
+"given location that have been assigned to a technician over the specified "
+"time period."
+msgstr ""
+
+#: views/account.py:26
+#, fuzzy
+msgid "Profile Settings"
+msgstr "Seaded"
+
+#: views/account.py:34 views/admin.py:235
+msgid "Settings saved"
+msgstr "Seaded salvestatud"
+
+#: views/account.py:47
+#, fuzzy
+msgid "Error in user details"
+msgstr "Ei leitud ühtegi toodet"
+
+#: views/account.py:73
+msgid "My Orders"
+msgstr "Minu tellimused"
+
+#: views/account.py:81
+msgid "Sign In"
+msgstr "Logi sisse"
+
+#: views/account.py:102
+#, python-format
+msgid "%s logged in"
+msgstr ""
+
+#: views/account.py:109
+msgid "Incorrect username or password"
+msgstr "Vale parool või kasutajanimi"
+
+#: views/account.py:111
+msgid "Login failed"
+msgstr ""
+
+#: views/account.py:119
+#, fuzzy
+msgid "You have logged out"
+msgstr "Logisid välja"
+
+#: views/account.py:223
+msgid "Users can only delete their own calendars!"
+msgstr "Kasutajad võivad kustutada ainult enda kalendreid"
+
+#: views/account.py:229
+msgid "Calendar deleted"
+msgstr "Kalender kustutatud"
+
+#: views/account.py:232
+msgid "Really delete this calendar?"
+msgstr ""
+
+#: views/account.py:251
+msgid "Calendar saved"
+msgstr "Kalender salvestatud"
+
+#: views/account.py:274
+msgid "Calendar event created"
+msgstr ""
+
+#: views/account.py:284
+msgid "Event saved"
+msgstr "Sündmus salvestatud"
+
+#: views/account.py:287
+#, fuzzy
+msgid "Edit Event"
+msgstr "Muud kasutajat"
+
+#: views/account.py:298
+msgid "Calendar event updated"
+msgstr ""
+
+#: views/account.py:305
+msgid "Users can only delete their own events!"
+msgstr ""
+
+#: views/account.py:313
+msgid "Calendar event deleted"
+msgstr "Sündmus kustutatud"
+
+#: views/account.py:316
+msgid "Really delete this event?"
+msgstr ""
+
+#: views/account.py:340
+msgid "Your registration is now pending approval."
+msgstr ""
+
+#: views/account.py:354
+#, fuzzy
+msgid "All notifications cleared"
+msgstr "saada teated mailiga"
+
+#: views/account.py:365 views/order.py:855
+msgid "Search query is too short"
+msgstr ""
+
+#: views/account.py:381
+msgid "Search results"
+msgstr "Otsingutulemused"
+
+#: views/admin.py:65 views/admin.py:270 views/customer.py:140
+#: views/device.py:152
+#, fuzzy, python-format
+msgid "%s saved"
+msgstr "Kasutaja salvestatud"
+
+#: views/admin.py:71
+msgid "GSX account for this sold-to and environment already exists"
+msgstr ""
+
+#: views/admin.py:83
+msgid "GSX account deleted"
+msgstr "GSX konto kustutatud"
+
+#: views/admin.py:126
+msgid "Checklist saved"
+msgstr ""
+
+#: views/admin.py:138
+msgid "Checklist deleted"
+msgstr ""
+
+#: views/admin.py:142
+msgid "Really delete this checklist?"
+msgstr ""
+
+#: views/admin.py:143
+msgid "This will also delete all checklist values."
+msgstr ""
+
+#: views/admin.py:179
+#, fuzzy, python-format
+msgid "Tag %s saved"
+msgstr "Kasutaja salvestatud"
+
+#: views/admin.py:194
+msgid "Tag deleted"
+msgstr ""
+
+#: views/admin.py:197
+msgid "Really delete this tag?"
+msgstr ""
+
+#: views/admin.py:215
+msgid "Check your settings"
+msgstr ""
+
+#: views/admin.py:283 views/admin.py:542 views/customer.py:154
+#, fuzzy, python-format
+msgid "%s deleted"
+msgstr "Kasutaja kustutatud"
+
+#: views/admin.py:322
+msgid "Field saved"
+msgstr "Väli salvestatud"
+
+#: views/admin.py:334
+msgid "Field deleted"
+msgstr "Väli kustutatud"
+
+#: views/admin.py:337
+msgid "Really delete this field?"
+msgstr ""
+
+#: views/admin.py:367
+#, fuzzy, python-format
+msgid "Template %s saved"
+msgstr "Põhi salvestatud"
+
+#: views/admin.py:383
+#, fuzzy, python-format
+msgid "Template %s deleted"
+msgstr "Põhi kustutatud"
+
+#: views/admin.py:386
+msgid "Really delete this template?"
+msgstr ""
+
+#: views/admin.py:427
+msgid "Group saved"
+msgstr "Rühm salvestatud"
+
+#: views/admin.py:439
+msgid "Group deleted"
+msgstr "Rühm kustutatud"
+
+#: views/admin.py:454
+msgid "User deleted"
+msgstr "Kasutaja kustutatud"
+
+#: views/admin.py:483
+#, fuzzy, python-format
+msgid "User %s saved"
+msgstr "Kasutaja salvestatud"
+
+#: views/admin.py:486
+msgid "Error in user profile data"
+msgstr ""
+
+#: views/admin.py:492
+#, fuzzy, python-format
+msgid "%d users"
+msgstr "Kasutajad"
+
+#: views/admin.py:527
+#, fuzzy, python-format
+msgid "Location %s saved"
+msgstr "Asukoht salvestatud"
+
+#: views/admin.py:548
+msgid "Really delete this location?"
+msgstr ""
+
+#: views/admin.py:549
+#, fuzzy
+msgid "This will not delete the orders at this location"
+msgstr "Toiming ei kustuta klienti kliendibaasist."
+
+#: views/admin.py:560
+msgid "Create, edit and delete service queues"
+msgstr ""
+
+#: views/admin.py:587
+msgid "Failed to save queue"
+msgstr ""
+
+#: views/admin.py:594
+#, fuzzy, python-format
+msgid "%s queue saved"
+msgstr "Kasutaja salvestatud"
+
+#: views/admin.py:611
+msgid "Queue deleted"
+msgstr "Järjekord kustutatud"
+
+#: views/admin.py:613
+msgid "Cannot delete queue"
+msgstr ""
+
+#: views/admin.py:633 views/admin.py:644
+#, fuzzy
+msgid "Access denied"
+msgstr "Lisavarustus"
+
+#: views/admin.py:637
+#, fuzzy
+msgid "Manage Sites"
+msgstr "Halda lisaseadmeid"
+
+#: views/admin.py:663
+#, fuzzy
+msgid "Site saved"
+msgstr "Märge salvestatud"
+
+#: views/admin.py:682
+#, fuzzy, python-format
+msgid "%d users imported"
+msgstr "Ei leitud ühtegi arvet"
+
+#: views/checkin.py:109
+msgid "Welcome"
+msgstr "Tere tulemast"
+
+#: views/checkin.py:120
+msgid "1/3: Enter serial number, IMEI code or choose a device"
+msgstr "1/3: Sisestage seerianumber, IMEI kood või valige seade"
+
+#: views/checkin.py:123
+msgid "Please enable cookies in your browser"
+msgstr ""
+
+#: views/checkin.py:134 views/checkin.py:161
+msgid "Your session has expired, please try again."
+msgstr ""
+
+#: views/checkin.py:140
+msgid "The serial number you entered is not valid."
+msgstr ""
+
+#: views/checkin.py:142
+msgid ""
+"Couldn't check warranty status, please choose device type manually or try "
+"again later."
+msgstr ""
+"Garantiistaatuse kontroll ebaõnnestus. Palun valige seade käsitsi või "
+"proovige hiljem uuesti."
+
+#: views/checkin.py:155
+msgid "1/3: Device details"
+msgstr "1/3: Seadme andmed"
+
+#: views/checkin.py:192
+msgid "2/3: Problem description"
+msgstr "2/3: Veakirjeldus"
+
+#: views/checkin.py:206
+msgid "3/3: Your contact details"
+msgstr "3/3/: Teie kontaktandmed"
+
+#: views/checkin.py:234
+msgid "Order does not exist"
+msgstr "Tellimust ei leitud"
+
+#: views/checkin.py:244
+msgid "2/3: Troubleshooting"
+msgstr "2/3: Veakirjeldus"
+
+#: views/checkin.py:281
+msgid "1/3: Choose your device"
+msgstr "1/3: Valige oma seade"
+
+#: views/checkin.py:284
+msgid "Apple Keyboard"
+msgstr "Apple klaviatuur"
+
+#: views/checkin.py:285
+msgid "Apple Wireless Device"
+msgstr "Apple juhtmeta seade"
+
+#: views/checkin.py:289
+#, python-format
+msgid "1/3: %s"
+msgstr "1/3: %s"
+
+#: views/checkin.py:320
+#, fuzzy
+msgid "Repair Status"
+msgstr "Remondid"
+
+#: views/checkin.py:332
+msgid "Waiting to be processed"
+msgstr "Ootab käsitlemist"
+
+#: views/checkin.py:334
+#, python-format
+msgid "Order %s not found"
+msgstr "Tellimust %s ei leitud"
+
+#: views/checkin.py:359
+msgid "Your session has expired"
+msgstr ""
+
+#: views/customer.py:89
+msgid "Customer added"
+msgstr "Klient lisatud"
+
+#: views/customer.py:105
+#, fuzzy
+msgid "Customer not found"
+msgstr "Kliendi andmed"
+
+#: views/customer.py:206
+msgid "Customer saved"
+msgstr "Klient salvestatud"
+
+#: views/customer.py:232
+msgid "Customer deleted"
+msgstr "Klient kustutatud"
+
+#: views/customer.py:250
+#, python-format
+msgid "Merge %s with"
+msgstr ""
+
+#: views/customer.py:265
+msgid "Customer records merged succesfully"
+msgstr ""
+
+#: views/customer.py:281
+#, python-format
+msgid "Customer %s moved to top level"
+msgstr ""
+
+#: views/customer.py:285
+#, fuzzy, python-format
+msgid "Customer %(customer)s moved to %(target)s"
+msgstr "Klient salvestatud"
+
+#: views/customer.py:307 views/device.py:282 views/order.py:875
+#: views/product.py:325 views/search.py:35
+#, fuzzy, python-format
+msgid "Search results for \"%s\""
+msgstr "Mitte ühtegi otsingutulemust"
+
+#: views/customer.py:357
+#, fuzzy
+msgid "Search for customers"
+msgstr "Otsi klienti"
+
+#: views/customer.py:376
+msgid "Please specify search query first"
+msgstr ""
+
+#: views/customer.py:419
+#, fuzzy
+msgid "Invalid upload data"
+msgstr "Puudulikud kliendi andmed"
+
+#: views/customer.py:439
+#, fuzzy, python-format
+msgid "%d customer(s) imported"
+msgstr "Ei leitud ühtegi arvet"
+
+#: views/device.py:114
+msgid "Device deleted"
+msgstr "Seade kustutatud"
+
+#: views/device.py:116
+msgid "Cannot delete device with GSX repairs"
+msgstr ""
+
+#: views/device.py:314
+#, python-format
+msgid "Devices matching \"%s\""
+msgstr ""
+
+#: views/device.py:333
+msgid "Device search"
+msgstr "Otsi seadmeid"
+
+#: views/device.py:386
+msgid "Invalid serial number for parts lookup"
+msgstr ""
+
+#: views/device.py:389
+msgid "Error calculating prices. Please check your system settings."
+msgstr ""
+
+#: views/device.py:486
+#, fuzzy, python-format
+msgid "%d devices imported"
+msgstr "Ei leitud ühtegi arvet"
+
+#: views/device.py:499
+msgid "Warranty status updated successfully"
+msgstr ""
+
+#: views/error.py:24
+#, fuzzy, python-format
+msgid "Browser: %s"
+msgstr "peab tagastama"
+
+#: views/gsx.py:21
+#, fuzzy, python-format
+msgid "Part %s updated"
+msgstr "Toode %s salvestatud"
+
+#: views/gsx.py:56
+#, python-format
+msgid "Part %(part)s added to repair %(repair)s"
+msgstr ""
+
+#: views/gsx.py:85
+#, fuzzy, python-format
+msgid "Part %(part)s removed from %(repair)s"
+msgstr "Eemaldatud järjekorrast"
+
+#: views/gsx.py:99
+#, fuzzy
+msgid "Submitted repairs cannot be deleted"
+msgstr "Suletud tellimusi ei või muuta"
+
+#: views/gsx.py:105
+#, fuzzy
+msgid "GSX repair deleted"
+msgstr "GSX konto kustutatud"
+
+#: views/gsx.py:147
+#, fuzzy
+msgid "Submitted repairs cannot be edited"
+msgstr "Suletud tellimusi ei või muuta"
+
+#: views/gsx.py:150
+msgid "Please add some parts before creating repair"
+msgstr ""
+
+#: views/gsx.py:153
+msgid "Cannot create GSX repair without valid customer data"
+msgstr ""
+
+#: views/gsx.py:190
+#, fuzzy
+msgid "GSX repair saved"
+msgstr "GSX parandused"
+
+#: views/gsx.py:216
+#, fuzzy
+msgid "Invalid component data"
+msgstr "Puudulikud kliendi andmed"
+
+#: views/gsx.py:231
+msgid "Invalid customer info"
+msgstr "Puudulikud kliendi andmed"
+
+#: views/gsx.py:296
+#, python-format
+msgid "%s serial numbers updated"
+msgstr ""
+
+#: views/invoices.py:91
+#, fuzzy, python-format
+msgid "Receipt #%d"
+msgstr "Prindi kviitung"
+
+#: views/invoices.py:99
+#, fuzzy, python-format
+msgid "Invoice %s"
+msgstr "Arved"
+
+#: views/invoices.py:110
+#, python-format
+msgid "Dispatch Order %s"
+msgstr ""
+
+#: views/invoices.py:135
+msgid "Walk-In Customer"
+msgstr ""
+
+#: views/note.py:170
+#, python-format
+msgid "Re: %s"
+msgstr ""
+
+#: views/note.py:237
+msgid "Note deleted"
+msgstr "Märge kustutatud"
+
+#: views/note.py:305
+msgid "OK"
+msgstr ""
+
+#: views/note.py:329
+#, python-format
+msgid "Notes containing \"%s\""
+msgstr ""
+
+#: views/note.py:373
+#, fuzzy
+msgid "Message search"
+msgstr "Teade saadetud"
+
+#: views/note.py:384
+#, fuzzy
+msgid "Edit Escalation"
+msgstr "Muuda GSX kontot"
+
+#: views/order.py:142
+#, python-format
+msgid "%d search results"
+msgstr "%d tulemust"
+
+#: views/order.py:153
+#, fuzzy, python-format
+msgid "Order %s"
+msgstr "Tellimused"
+
+#: views/order.py:212
+#, fuzzy, python-format
+msgid "Order %s reopened"
+msgstr "Tellimus %s loodud"
+
+#: views/order.py:349
+#, python-format
+msgid "Repair %s marked complete."
+msgstr ""
+
+#: views/order.py:404
+#, python-format
+msgid "Order %s deleted"
+msgstr "Tellimus %s kustutatud"
+
+#: views/order.py:408
+#, python-format
+msgid "Cannot delete order %(order)s: %(error)s"
+msgstr ""
+
+#: views/order.py:418
+msgid "Follow"
+msgstr "Jälgi tellimust"
+
+#: views/order.py:424
+msgid "Unfollow"
+msgstr "Ära jälgi tellimust"
+
+#: views/order.py:461
+msgid "Closed orders cannot be updated"
+msgstr "Suletud tellimusi ei või muuta"
+
+#: views/order.py:475
+#, python-format
+msgid "User %s does not exist"
+msgstr ""
+
+#: views/order.py:503
+#, python-format
+msgid "Label %s does not exist"
+msgstr ""
+
+#: views/order.py:508 views/order.py:514 views/order.py:523
+msgid "Order updated"
+msgstr "Tellimus uuendatud"
+
+#: views/order.py:520
+#, python-format
+msgid "Order moved to %s"
+msgstr "Tellimus tõsteti järjekorda %s"
+
+#: views/order.py:536
+#, python-format
+msgid "Service Order #%s"
+msgstr "Hooldustellimus #%s"
+
+#: views/order.py:633
+#, python-format
+msgid "Products of order %s reserved"
+msgstr ""
+
+#: views/order.py:674 views/product.py:271
+#, python-format
+msgid "Product %s saved"
+msgstr "Toode %s salvestatud"
+
+#: views/order.py:761
+#, fuzzy
+msgid "Order item does not exist"
+msgstr "Tellimust ei leitud"
+
+#: views/order.py:842
+#, python-format
+msgid "Customer %s removed"
+msgstr "Klient %s eemaldatud"
+
+#: views/product.py:117
+msgid "Parts database uploaded for processing"
+msgstr ""
+
+#: views/product.py:206
+#, python-format
+msgid "%d products imported"
+msgstr "%d toodet edukalt imporditud"
+
+#: views/product.py:211
+#, fuzzy
+msgid "Upload products"
+msgstr "Saabuvad tooted"
+
+#: views/product.py:274
+msgid "Error in inventory details"
+msgstr ""
+
+#: views/product.py:276
+#, fuzzy
+msgid "Error in product info"
+msgstr "Ei leitud ühtegi toodet"
+
+#: views/product.py:296
+msgid "Product deleted"
+msgstr "Toode kustutatud"
+
+#: views/product.py:298
+#, fuzzy
+msgid "Cannot delete product"
+msgstr "Loo toode"
+
+#: views/product.py:371
+#, fuzzy, python-format
+msgid "Category %s already exists"
+msgstr "Tooterühm salvestatud"
+
+#: views/product.py:373
+#, fuzzy, python-format
+msgid "Category %s saved"
+msgstr "Tooterühm salvestatud"
+
+#: views/product.py:389
+msgid "Category deleted"
+msgstr "Tooterühm kustutatud"
+
+#: views/product.py:437
+msgid "Price info updated from GSX"
+msgstr ""
+
+#: views/product.py:439
+msgid "Failed to update price from GSX"
+msgstr ""
+
+#: views/purchases.py:81
+#, python-format
+msgid "Product %s removed"
+msgstr "Toode %s eemaldatud"
+
+#: views/purchases.py:96
+#, fuzzy, python-format
+msgid "Purchase Order %d"
+msgstr "Ostutellimus"
+
+#: views/purchases.py:132
+#, fuzzy, python-format
+msgid "Purchase Order %d saved"
+msgstr "Ostutellimus %d loodud"
+
+#: views/purchases.py:136
+#, fuzzy, python-format
+msgid "Purchase Order %d submitted"
+msgstr "Ostutellimus %d loodud"
+
+#: views/purchases.py:144
+#, fuzzy, python-format
+msgid "Purchase Order #%d"
+msgstr "Ostutellimus"
+
+#: views/purchases.py:159
+#, fuzzy, python-format
+msgid "Purchase Order %s has already been submitted"
+msgstr "Ostutellimus %d on juba kinnitatud."
+
+#: views/purchases.py:177
+#, python-format
+msgid "Products ordered with confirmation %s"
+msgstr ""
+
+#: views/purchases.py:193
+#, python-format
+msgid "Purchase Order %s deleted"
+msgstr ""
+
+#: views/shipments.py:83
+#, fuzzy, python-format
+msgid "%d incoming products"
+msgstr "Mitte ühtegi saavuvad toodet"
+
+#: views/shipments.py:113
+#, python-format
+msgid "%d products received"
+msgstr ""
+
+#: views/shipments.py:152
+#, python-format
+msgid "Product %s received"
+msgstr ""
+
+#: views/shipments.py:186
+#, fuzzy
+msgid "Browse Bulk Returns"
+msgstr "peab tagastama"
+
+#: views/shipments.py:210
+#, fuzzy
+msgid "View bulk return"
+msgstr "peab tagastama"
+
+#: views/shipments.py:223
+#, fuzzy, python-format
+msgid "Location %s has no Ship-To"
+msgstr "Asukoht salvestatud"
+
+#: views/shipments.py:246
+#, fuzzy
+msgid "Bulk return saved"
+msgstr "Järjekord salvestatud"
+
+#: views/shipments.py:251
+#, fuzzy, python-format
+msgid "Bulk return %s submitted"
+msgstr "Järjekord salvestatud"
+
+#: views/shipments.py:266
+#, python-format
+msgid "%d parts pending return"
+msgstr ""
+
+#: views/shipments.py:279
+#, fuzzy, python-format
+msgid "Part %s removed from bulk return"
+msgstr "Eemaldatud järjekorrast"
+
+#: views/shipments.py:296
+#, python-format
+msgid "Part %s added to return"
+msgstr ""
+
+#: views/shipments.py:322
+msgid "Return DOA Part"
+msgstr ""
+
+#: views/shipments.py:331
+msgid "This part will be converted to regular inventory"
+msgstr ""
+
+#: views/shipments.py:346
+msgid "Part updated"
+msgstr ""
+
+#: views/stats.py:241
+msgid "Time Scale"
+msgstr ""
+
+#: views/stats.py:465
+msgid "No Queue"
+msgstr "Ilma järjekorrata"
+
+#, fuzzy
+#~ msgid "Error in SMS gateway settings"
+#~ msgstr "Ei leitud ühtegi toodet"
+
+#~ msgid "No device"
+#~ msgstr "Ilma seadmeta"
+
+#~ msgid "just now"
+#~ msgstr "hetk tagasi"
+
+#~ msgid "today at %(time)s"
+#~ msgstr "täna kell %(time)s"
+
+#~ msgid "yesterday at %(time)s"
+#~ msgstr "eile kell %(time)s"
+
+#~ msgid "%a, %d.%m at %k:%M"
+#~ msgstr "%a, %d.%m kell %k:%M"
+
+#, fuzzy
+#~ msgid "Exchange"
+#~ msgstr "Vahetushind"
diff --git a/servo/locale/fi_FI/LC_MESSAGES/django.mo b/servo/locale/fi_FI/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e6e118c
--- /dev/null
+++ b/servo/locale/fi_FI/LC_MESSAGES/django.mo
Binary files differ
diff --git a/servo/locale/fi_FI/LC_MESSAGES/django.po b/servo/locale/fi_FI/LC_MESSAGES/django.po
new file mode 100644
index 0000000..d5a5c0c
--- /dev/null
+++ b/servo/locale/fi_FI/LC_MESSAGES/django.po
@@ -0,0 +1,5396 @@
+# FINNISH TRANSLATION FOR SERVO
+# Copyright (C) 2013 First Party Software LTD
+# This file is distributed under the same license as the PACKAGE package.
+# Filipp Lepalaan <filipp@fpsw.ee>, 2013.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-08-23 10:41+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms/account.py:36 forms/account.py:70 forms/account.py:78
+#: forms/admin.py:364 forms/admin.py:407 forms/admin.py:435
+#: models/account.py:138 models/common.py:197
+#: templates/devices/get_info.html:33 templates/devices/summary.html:24
+#: templates/orders/devices.html:41
+msgid "Password"
+msgstr "Salasana"
+
+#: forms/account.py:41 templates/checkin/confirmation.html:6
+#: templates/orders/repair.html:28 templates/products/receive_item.html:33
+#: templates/purchases/list_pos.html:55 templates/purchases/view_po.html:23
+#: templates/shipments/list_incoming.html:19
+msgid "Confirmation"
+msgstr "Vahvistus"
+
+#: forms/account.py:54
+msgid "Password and confirmation do not match!"
+msgstr "Salasana ja vahvistus eivät täsmää!"
+
+#: forms/account.py:61
+msgid "File size of photo is too large"
+msgstr "Kuvan tiedostokoko on liian iso"
+
+#: forms/account.py:67
+msgid "First Name"
+msgstr "Etunimi"
+
+#: forms/account.py:68
+msgid "Last Name"
+msgstr "Sukunimi"
+
+#: forms/account.py:69 templates/accounts/print_calendar.html:23
+#: templates/orders/print_confirmation.html:24
+#: templates/orders/print_confirmation.html:54
+#: templates/orders/print_receipt.html:12
+msgid "Email Address"
+msgstr "Sähköposti"
+
+#: forms/account.py:75 forms/admin.py:358 forms/admin.py:403
+#: forms/admin.py:431 templates/devices/get_info.html:29
+#: templates/devices/summary.html:20 templates/orders/devices.html:37
+msgid "Username"
+msgstr "Tunnus"
+
+#: forms/admin.py:43 views/product.py:173
+msgid "Unsupported file encoding"
+msgstr ""
+
+#: forms/admin.py:90
+msgid "Group members"
+msgstr "Ryhmän jäsenet"
+
+#: forms/admin.py:97 templates/admin/users/form.html:9
+msgid "Permissions"
+msgstr "Oikeudet"
+
+#: forms/admin.py:133
+msgid "A location with that name already exists"
+msgstr ""
+
+#: forms/admin.py:192
+#, python-format
+msgid "\"%s\" cannot be used as a username"
+msgstr ""
+
+#: forms/admin.py:234
+#, python-format
+msgid "Syntax error in template: %s"
+msgstr ""
+
+#: forms/admin.py:241
+msgid "Company Name"
+msgstr "Yrityksen nimi"
+
+#: forms/admin.py:243
+#, fuzzy
+msgid "Company Logo"
+msgstr "Yrityksen nimi"
+
+#: forms/admin.py:245
+msgid "Company-wide logo to use in print templates"
+msgstr ""
+
+#: forms/admin.py:250
+msgid "Terms of Service"
+msgstr "Palveluehdot"
+
+#: forms/admin.py:252
+msgid ""
+"These terms will be added to your work confirmations and public check-in "
+"site."
+msgstr ""
+
+#: forms/admin.py:258
+msgid "Autocomplete GSX repairs"
+msgstr "Sulje GSX korjaus automaattisesti"
+
+#: forms/admin.py:259
+msgid "Complete the GSX repair when closing a Service Order"
+msgstr "Sulje GSX korjaus tilausta suljettaessa"
+
+#: forms/admin.py:264 forms/admin.py:371
+#, fuzzy
+msgid "User Account"
+msgstr "Luo uusi tili"
+
+#: forms/admin.py:266
+msgid "User account to use for the public check-in service"
+msgstr ""
+
+#: forms/admin.py:270 templates/admin/users/groups.html:12 views/stats.py:276
+msgid "Group"
+msgstr "Ryhmä"
+
+#: forms/admin.py:272
+msgid "Users to choose from in the check-in interface"
+msgstr ""
+
+#: forms/admin.py:276 models/repair.py:54
+#, fuzzy
+msgid "Checklist"
+msgstr "Muistilistat"
+
+#: forms/admin.py:278
+msgid "Checklist to show during check-in"
+msgstr ""
+
+#: forms/admin.py:282 models/queue.py:174
+msgid "Queue"
+msgstr "Jono"
+
+#: forms/admin.py:284
+msgid "Orders created through the check-in interface will go into this queue"
+msgstr "Check-in liittymän kautta luodut tilaukset menevät tähän jonoon"
+
+#: forms/admin.py:289
+msgid "Show timeline"
+msgstr ""
+
+#: forms/admin.py:290
+msgid "Show status timeline on public repair status page"
+msgstr ""
+
+#: forms/admin.py:295
+#, fuzzy
+msgid "Show password"
+msgstr "salasana"
+
+#: forms/admin.py:296
+msgid "Make checkin device password field readable"
+msgstr ""
+
+#: forms/admin.py:301
+#, fuzzy
+msgid "Show checklist results"
+msgstr "Muistilista poistettu"
+
+#: forms/admin.py:302
+msgid "Show checklist results in order confirmation"
+msgstr ""
+
+#: forms/admin.py:306
+msgid "Currency"
+msgstr "Valuutta"
+
+#: forms/admin.py:319
+msgid "Default GSX account"
+msgstr "Oletus GSX-tili"
+
+#: forms/admin.py:321
+msgid "Use this GSX account before and order is assigned to a queue"
+msgstr "Käytä tätä GSX tiliä ilman jonoa"
+
+#: forms/admin.py:327 models/product.py:107 models/product.py:134
+msgid "Margin %"
+msgstr "Kate %"
+
+#: forms/admin.py:328
+msgid "Default margin for new products"
+msgstr "Oletus-kate uusille tuotteille"
+
+#: forms/admin.py:334 models/product.py:88
+msgid "VAT %"
+msgstr "ALV %"
+
+#: forms/admin.py:335
+msgid "Default VAT for new products"
+msgstr "Oletus-verokanta uusille tuotteille"
+
+#: forms/admin.py:341
+msgid "Shipping Cost"
+msgstr "Kuljetuskulut"
+
+#: forms/admin.py:342
+msgid "Default shipping cost for new products"
+msgstr "Oletus-kuljetusmaksu uusille tuotteille"
+
+#: forms/admin.py:348
+msgid "Track inventory"
+msgstr "Varastoseuranta"
+
+#: forms/admin.py:349
+msgid "Unchecking this will disable tracking product amounts in your inventory"
+msgstr ""
+
+#: forms/admin.py:353
+msgid "IMAP server"
+msgstr "IMAP palvelin"
+
+#: forms/admin.py:368 forms/admin.py:410 forms/admin.py:445
+msgid "Use SSL"
+msgstr "Käytä salausta"
+
+#: forms/admin.py:373
+msgid "User account to use when creating notes from messages"
+msgstr ""
+
+#: forms/admin.py:378
+#, fuzzy
+msgid "Default Sender"
+msgstr "oletus"
+
+#: forms/admin.py:380 models/account.py:241
+#, fuzzy
+msgid "User"
+msgstr "Käyttäjät"
+
+#: forms/admin.py:381 forms/returns.py:53 models/common.py:507
+#: templates/admin/users/form.html:10 views/stats.py:265
+msgid "Location"
+msgstr "Toimipaikka"
+
+#: forms/admin.py:382
+#, fuzzy
+msgid "Custom..."
+msgstr "Asiakas"
+
+#: forms/admin.py:384
+msgid "Select the default sender address for outgoing emails"
+msgstr ""
+
+#: forms/admin.py:396
+msgid "Default subject"
+msgstr "Oletus-otsikko"
+
+#: forms/admin.py:401
+msgid "SMTP server"
+msgstr "SMTP Palvelin"
+
+#: forms/admin.py:413
+msgid "SMS Gateway"
+msgstr "SMS-palvelin"
+
+#: forms/admin.py:415
+msgid "Built-in"
+msgstr ""
+
+#: forms/admin.py:423 forms/admin.py:456 forms/checkin.py:100
+msgid "Email address"
+msgstr "Sähköposti"
+
+#: forms/admin.py:426
+msgid "URL"
+msgstr "URL"
+
+#: forms/admin.py:428
+msgid "SMS Server URL"
+msgstr "SMS-palvelimen osoite"
+
+#: forms/admin.py:441
+#, fuzzy
+msgid "Sender"
+msgstr "oletus"
+
+#: forms/admin.py:451
+#, fuzzy
+msgid "Notify locations"
+msgstr "Ilmoitukset"
+
+#: forms/admin.py:452
+msgid "Daily reports will be sent to the location's email address"
+msgstr ""
+
+#: forms/admin.py:457
+msgid "Send daily reports to this email address"
+msgstr "Lähetä päivittäiset raportit tähän osoitteeseen"
+
+#: forms/admin.py:465
+msgid "Invalid margin %"
+msgstr "Väärä kateprosentti"
+
+#: forms/checkin.py:26 models/note.py:54
+#, python-format
+msgid "%s is not a valid phone number"
+msgstr "%s ei ole sallittu puhelinnumero"
+
+#: forms/checkin.py:31
+#, python-format
+msgid "%s is not a valid serial or IMEI number"
+msgstr "%s ei ole validi sarjanumero tai IMEI koodi"
+
+#: forms/checkin.py:40 forms/repairs.py:22 models/customer.py:99
+#: templates/customers/view.html:38
+msgid "Country"
+msgstr "Maa"
+
+#: forms/checkin.py:46 templates/orders/devices.html:54
+#: templates/orders/print_confirmation.html:79
+msgid "Accessories"
+msgstr "Lisälaitteet"
+
+#: forms/checkin.py:48
+msgid ""
+"Please list here any accessories you'd like to check in with your device "
+"(cables, power adapters, bags, etc)"
+msgstr ""
+"Ole hyvä ja listaa tähän oheislaitteet jotka jätät huoltoon laitteen mukana "
+"(kuten esim. laturi, kaapelit, laukut yms)"
+
+#: forms/checkin.py:53
+msgid "Proof of Purchase"
+msgstr "Ostotodistus"
+
+#: forms/checkin.py:54
+msgid "Proof of Purchase is required when setting purchase date manually"
+msgstr "Ostotodistus on tarpeellinen jos muutat laitteen ostopäivämäärää"
+
+#: forms/checkin.py:93 forms/repairs.py:15
+msgid "First name"
+msgstr "Etunimi"
+
+#: forms/checkin.py:94 forms/repairs.py:16
+msgid "Last name"
+msgstr "Sukunimi"
+
+#: forms/checkin.py:97
+msgid "Company (optional)"
+msgstr "Yritys (ei pakollinen)"
+
+#: forms/checkin.py:104
+msgid "Phone number"
+msgstr "Puhelinnumero"
+
+#: forms/checkin.py:107 forms/repairs.py:19 models/invoices.py:75
+#: templates/customers/find.html:51 templates/customers/view.html:30
+#: templates/orders/print_confirmation.html:57
+#: templates/orders/print_receipt.html:15
+msgid "Address"
+msgstr "Katuosoite"
+
+#: forms/checkin.py:108 forms/repairs.py:21 templates/customers/find.html:53
+msgid "City"
+msgstr "Toimipaikka"
+
+#: forms/checkin.py:109 templates/customers/find.html:52
+msgid "Postal Code"
+msgstr "Postinumero"
+
+#: forms/checkin.py:112
+msgid "Check-in location"
+msgstr "Vastaanottosijainti"
+
+#: forms/checkin.py:115
+msgid "Choose where you want to leave the device"
+msgstr "Valitse miten haluat toimittaa laitteen huoltoon"
+
+#: forms/checkin.py:119
+msgid "Check-out location"
+msgstr "Luovutus-sijainti"
+
+#: forms/checkin.py:122
+msgid "Choose where you want to pick up the device"
+msgstr "Valitse miten haluat laitteen takaisin"
+
+#: forms/checkin.py:124
+msgid ""
+"I agree to the <a href=\"/checkin/terms/\" target=\"_blank\">terms of "
+"service.</a>"
+msgstr ""
+"Hyväksyn <a href=\"/checkin/terms/\" target=\"_blank\">palveluehdot.</a>"
+
+#: forms/checkin.py:139
+msgid "Serial number or IMEI"
+msgstr "Sarjanumero tai IMEI-koodi"
+
+#: forms/checkin.py:146 templates/notes/view_note.html:21
+#: templates/products/receive_item.html:17
+#: templates/repairs/get_details.html:10
+#: templates/search/results/gsx_repair_details.html:6
+#: templates/shipments/list_incoming.html:18
+msgid "Service Order"
+msgstr "Huoltotilaus"
+
+#: forms/checkin.py:147
+msgid "Invalid Service Order number"
+msgstr "Väärä huoltotilausnumero"
+
+#: forms/checkin.py:155 templates/checkin/issue.html:7
+msgid "Problem description"
+msgstr "Ongelmakuvaus"
+
+#: forms/checkin.py:160 templates/orders/repair.html:23
+msgid "Attachment"
+msgstr "Liite"
+
+#: forms/checkin.py:161
+msgid "Please use this to attach relevant documents"
+msgstr "Ole hyvä ja lisää tähän tarvittavat dokumentit"
+
+#: forms/checkin.py:166 models/common.py:457
+#: templates/accounts/print_calendar.html:45
+#: templates/accounts/view_calendar.html:32 templates/customers/view.html:42
+#: templates/devices/get_info.html:41
+#: templates/devices/search_gsx_warranty.html:48
+#: templates/devices/summary.html:28 templates/notes/list_notes.html:74
+#: templates/orders/devices.html:49 templates/orders/repair.html:32
+#: templates/repairs/get_details.html:16 templates/search/spotlight.html:27
+#: templates/search/results/gsx_repair_details.html:12
+msgid "Notes"
+msgstr "Merkinnät"
+
+#: forms/checkin.py:168
+msgid "Will not appear on the print-out"
+msgstr ""
+
+#: forms/customer.py:35
+#, fuzzy
+msgid "Enter a valid phone number"
+msgstr "%s ei ole sallittu puhelinnumero"
+
+#: forms/customer.py:44 forms/product.py:17
+msgid "Name contains"
+msgstr "Nimi sisältää"
+
+#: forms/customer.py:48
+msgid "Email contains"
+msgstr "Sähköposti sisältää"
+
+#: forms/customer.py:52
+msgid "Address contains"
+msgstr "Osoite sisältää"
+
+#: forms/customer.py:56
+#, fuzzy
+msgid "Checked in between"
+msgstr "Tilaus %s luotu"
+
+#: forms/customer.py:72
+msgid "Please specify at least one parameter"
+msgstr ""
+
+#: forms/customer.py:76
+#, fuzzy
+msgid "CSV file"
+msgstr "tiedosto"
+
+#: forms/customer.py:80
+msgid "Skip duplicates"
+msgstr ""
+
+#: forms/customer.py:81
+msgid "Skip customers with existing email addresses"
+msgstr ""
+
+#: forms/customer.py:87
+msgid "Data file should be in text format"
+msgstr ""
+
+#: forms/devices.py:26 forms/orders.py:97
+#, fuzzy
+msgid "Created between"
+msgstr "Tilaus %s luotu"
+
+#: forms/devices.py:34
+#, fuzzy
+msgid "Serial number contains"
+msgstr "Lähettäjä sisältää"
+
+#: forms/devices.py:42 forms/product.py:25
+msgid "Description contains"
+msgstr "Kuvaus sisältää"
+
+#: forms/devices.py:61
+#, fuzzy
+msgid "Serial numbers may not include spaces"
+msgstr "Lähettäjä sisältää"
+
+#: forms/invoices.py:39 forms/orders.py:57 forms/product.py:125
+msgid "State is"
+msgstr "Tila on"
+
+#: forms/invoices.py:41 forms/invoices.py:50 forms/product.py:127
+msgid "Any"
+msgstr "Kaikki"
+
+#: forms/invoices.py:42 forms/product.py:128 models/order.py:128
+msgid "Open"
+msgstr "Avoin"
+
+#: forms/invoices.py:43
+msgid "Paid"
+msgstr "Maksettu"
+
+#: forms/invoices.py:49
+msgid "Payment method is"
+msgstr "Maksutapa on"
+
+#: forms/invoices.py:55
+msgid "Status is not"
+msgstr "Status ei ole"
+
+#: forms/invoices.py:61 forms/invoices.py:64 forms/product.py:140
+#: forms/product.py:143 forms/product.py:173 forms/product.py:187
+#: views/stats.py:250
+msgid "Start date"
+msgstr "Alku"
+
+#: forms/invoices.py:69 forms/invoices.py:72 forms/product.py:148
+#: forms/product.py:151 forms/product.py:180 forms/product.py:194
+#: views/stats.py:255
+msgid "End date"
+msgstr "Loppu"
+
+#: forms/invoices.py:77
+msgid "Customer name contains"
+msgstr "Asiakkaan nimi sisältää"
+
+#: forms/invoices.py:81 forms/notes.py:43
+msgid "Service Order is"
+msgstr "Huoltotilaus on"
+
+#: forms/notes.py:29 models/note.py:81 templates/notes/view_note.html:19
+msgid "From"
+msgstr "Lähettäjä"
+
+#: forms/notes.py:40
+msgid "Body contains"
+msgstr "Viesti sisältää"
+
+#: forms/notes.py:41
+msgid "Recipient contains"
+msgstr "Saaja sisältää"
+
+#: forms/notes.py:42
+msgid "Sender contains"
+msgstr "Lähettäjä sisältää"
+
+#: forms/orders.py:52 forms/product.py:165
+#, fuzzy
+msgid "Location is"
+msgstr "Toimipaikat"
+
+#: forms/orders.py:62
+msgid "Queue is"
+msgstr "Jono on"
+
+#: forms/orders.py:67 models/queue.py:236 templates/admin/queues/form.html:27
+#: templates/devices/search_gsx_repairs.html:9
+#: templates/invoices/index.html:47 templates/orders/list.html:14
+#: templates/repairs/get_details.html:20
+#: templates/search/results/gsx_repair_details.html:16 views/stats.py:286
+msgid "Status"
+msgstr "Status"
+
+#: forms/orders.py:72
+msgid "Created by"
+msgstr "Luoja"
+
+#: forms/orders.py:77 templates/orders/list.html:13
+msgid "Assigned to"
+msgstr "Käsittelijä"
+
+#: forms/orders.py:82 templates/orders/edit.html:91
+msgid "Label"
+msgstr "Merkki"
+
+#: forms/orders.py:87 models/common.py:345
+msgid "Green"
+msgstr "Vihreä"
+
+#: forms/orders.py:88
+msgid "Yellow"
+msgstr "Keltainen"
+
+#: forms/orders.py:89 models/common.py:347
+msgid "Red"
+msgstr "Punainen"
+
+#: forms/orders.py:90
+msgid "Grey"
+msgstr "Harmaa"
+
+#: forms/orders.py:92
+msgid "Color"
+msgstr "Väri"
+
+#: forms/product.py:21
+#, fuzzy
+msgid "Code contains"
+msgstr "Viesti sisältää"
+
+#: forms/product.py:29
+#, fuzzy
+msgid "Device model is"
+msgstr "laitemallit"
+
+#: forms/product.py:40
+msgid "Product datafile"
+msgstr "Tuotetiedosto"
+
+#: forms/product.py:48
+msgid "Parts database file"
+msgstr "Varaosatiedosto"
+
+#: forms/product.py:52
+msgid "Import vintage parts"
+msgstr "Tuo vintage osat"
+
+#: forms/product.py:57
+msgid "Update product prices"
+msgstr "Päivitä hinnat"
+
+#: forms/product.py:112
+#, python-format
+msgid "Product code %s contains invalid characters"
+msgstr ""
+
+#: forms/product.py:129
+msgid "Submitted"
+msgstr "Lähetetty"
+
+#: forms/product.py:130 templates/products/receive_item.html:26
+#: templates/products/view.html:158 templates/purchases/list_pos.html:56
+msgid "Received"
+msgstr "Saavutetut"
+
+#: forms/product.py:156
+#, fuzzy
+msgid "Reference contains"
+msgstr "Lähettäjä sisältää"
+
+#: forms/product.py:170
+#, fuzzy
+msgid "Ordered between"
+msgstr "Tilaus %s luotu"
+
+#: forms/product.py:184
+#, fuzzy
+msgid "Received between"
+msgstr "Vastaanotettu"
+
+#: forms/product.py:198
+#, fuzzy
+msgid "Confirmation is"
+msgstr "Vahvistus"
+
+#: forms/product.py:201
+#, fuzzy
+msgid "Service order is"
+msgstr "Huoltotilaus on"
+
+#: forms/repairs.py:17 models/invoices.py:69 templates/customers/find.html:49
+#: templates/customers/view.html:22
+msgid "Email"
+msgstr "Sähköposti"
+
+#: forms/repairs.py:18 models/invoices.py:63
+#: templates/accounts/print_calendar.html:24 templates/customers/find.html:50
+#: templates/customers/search.html:18 templates/customers/view.html:26
+#: templates/orders/print_confirmation.html:25
+#: templates/search/results/customers.html:14
+msgid "Phone"
+msgstr "Puhelin"
+
+#: forms/repairs.py:20 models/common.py:413 models/customer.py:88
+msgid "ZIP Code"
+msgstr "Postinumero"
+
+#: forms/repairs.py:23 models/common.py:325 models/product.py:224
+msgid "Other"
+msgstr "Muu"
+
+#: forms/repairs.py:58 models/repair.py:103
+msgid "Technician"
+msgstr ""
+
+#: forms/repairs.py:73
+msgid "Replacement serial number must be set"
+msgstr ""
+
+#: forms/returns.py:16
+msgid "Reason"
+msgstr "Syy"
+
+#: forms/returns.py:18 forms/returns.py:29 forms/returns.py:39
+msgid "Select..."
+msgstr "Valitse..."
+
+#: forms/returns.py:19
+msgid "Part not needed"
+msgstr "Varaosaa ei tarvittu"
+
+#: forms/returns.py:20
+msgid "Duplicated part"
+msgstr "Varaosa tuplana"
+
+#: forms/returns.py:21
+msgid "Added wrong part"
+msgstr "Lisätty väärä osa"
+
+#: forms/returns.py:22
+msgid "Tried to cancel order"
+msgstr ""
+
+#: forms/returns.py:23
+msgid "Customer refused order"
+msgstr "Asiakas peruutti huollon"
+
+#: forms/returns.py:27
+msgid "Type"
+msgstr "Tyyppi"
+
+#: forms/returns.py:30
+msgid "Diagnostic"
+msgstr "Diagnoosi"
+
+#: forms/returns.py:31
+msgid "Un-Opened"
+msgstr "Avaamaton"
+
+#: forms/returns.py:38 models/parts.py:46
+msgid "Symptom Code"
+msgstr "Oirekoodi"
+
+#: forms/returns.py:42 models/parts.py:51
+msgid "Symptom Modifier"
+msgstr "Toistuvuus"
+
+#: management/commands/cron.py:81 models/order.py:127
+msgid "Unassigned"
+msgstr "Odottava"
+
+#: management/commands/cron.py:82
+msgid "No Status"
+msgstr "Ei statusta"
+
+#: management/commands/cron.py:86
+#, python-format
+msgid "Repairs aging beyond limits at %s"
+msgstr ""
+
+#: management/commands/cron.py:115
+msgid "Products stocked below limit"
+msgstr "Tuotteita alle minimirajan"
+
+#: messaging/sms.py:14
+#, fuzzy
+msgid "SMS sender not configured"
+msgstr "Oletus GSX-tili"
+
+#: messaging/sms.py:51
+msgid "SMS sender name not configured"
+msgstr ""
+
+#: messaging/sms.py:77
+#, python-format
+msgid "Failed to send message to %s"
+msgstr ""
+
+#: messaging/sms.py:90
+msgid "Wrong sender name"
+msgstr ""
+
+#: messaging/sms.py:91
+msgid "Too many messages in one request"
+msgstr ""
+
+#: messaging/sms.py:92
+#, fuzzy
+msgid "Invalid username or password"
+msgstr "Väärä tunnus tai salasana"
+
+#: messaging/sms.py:93
+msgid "Insufficient credits on your account"
+msgstr ""
+
+#: messaging/sms.py:94
+msgid "Unsuccessful message submission"
+msgstr ""
+
+#: messaging/sms.py:95 messaging/sms.py:96
+msgid "Internal system error"
+msgstr ""
+
+#: messaging/sms.py:118
+#, python-format
+msgid "Unknown error (%s)"
+msgstr ""
+
+#: messaging/sms.py:132
+msgid "No SMS HTTP gateway defined"
+msgstr ""
+
+#: models/account.py:53 templates/admin/users/index.html:7
+msgid "New User"
+msgstr "Uusi käyttäjä"
+
+#: models/account.py:64 templates/orders/edit.html:33
+msgid "Current Location"
+msgstr "Nykyinen sijainti"
+
+#: models/account.py:65
+msgid "Orders you create will be registered to this location."
+msgstr ""
+
+#: models/account.py:71
+msgid "queues"
+msgstr "jonot"
+
+#: models/account.py:74
+#, fuzzy
+msgid "Danish"
+msgstr "Suomi"
+
+#: models/account.py:75
+msgid "Dutch"
+msgstr ""
+
+#: models/account.py:76
+msgid "English"
+msgstr "Englanti"
+
+#: models/account.py:77
+msgid "Estonian"
+msgstr "Viro"
+
+#: models/account.py:78
+msgid "Finnish"
+msgstr "Suomi"
+
+#: models/account.py:79
+msgid "Swedish"
+msgstr "Ruotsi"
+
+#: models/account.py:85
+msgid "language"
+msgstr "kieli"
+
+#: models/account.py:86
+msgid "Select which language you want to use Servo in."
+msgstr ""
+
+#: models/account.py:94 models/common.py:428
+#, fuzzy
+msgid "Time zone"
+msgstr "aikavyöhyke"
+
+#: models/account.py:98
+msgid "Denmark"
+msgstr "Tanska"
+
+#: models/account.py:99
+msgid "Estonia"
+msgstr "Viro"
+
+#: models/account.py:100
+msgid "Finland"
+msgstr "Suomi"
+
+#: models/account.py:101
+msgid "United States"
+msgstr "Yhdysvallat"
+
+#: models/account.py:102
+msgid "Netherlands"
+msgstr ""
+
+#: models/account.py:103
+msgid "Sweden"
+msgstr "Ruotsi"
+
+#: models/account.py:109
+msgid "region"
+msgstr "alue"
+
+#: models/account.py:110
+msgid "Affects formatting of numbers, dates and currencies."
+msgstr ""
+
+#: models/account.py:114
+msgid "email notifications"
+msgstr "Lähetä ilmoitukset sähköpostilla"
+
+#: models/account.py:115
+msgid "Event notifications will also be emailed to you."
+msgstr "Ilmoitukset lähetetään myös sähköpostitse."
+
+#: models/account.py:119
+msgid "print automatically"
+msgstr "tulosta automaattisesti"
+
+#: models/account.py:120
+msgid "Opens print dialog automatically."
+msgstr ""
+
+#: models/account.py:126
+msgid "tech ID"
+msgstr "Tech ID"
+
+#: models/account.py:132 models/common.py:190
+msgid "User ID"
+msgstr "Käyttäjätunnus"
+
+#: models/account.py:144
+msgid "PO prefix"
+msgstr ""
+
+#: models/account.py:145
+#, fuzzy
+msgid "GSX repairs you create will be prefixed"
+msgstr "GSX korjaus %s luotu"
+
+#: models/account.py:152 models/customer.py:107 models/device.py:152
+#: models/product.py:202
+msgid "photo"
+msgstr "valokuva"
+
+#: models/account.py:153
+msgid "Maximum avatar size is 1MB"
+msgstr "Kuvatiedoston maksimikoko on 1Mt"
+
+#: models/account.py:242 templates/admin/index.html:24 views/admin.py:405
+msgid "Users & Groups"
+msgstr "Käyttäjät ja ryhmät"
+
+#: models/calendar.py:25 models/common.py:632 models/common.py:709
+#: models/order.py:704 models/repair.py:32
+msgid "title"
+msgstr "nimike"
+
+#: models/calendar.py:26 templates/accounts/calendars.html:7
+msgid "New Calendar"
+msgstr "Uusi kalenteri"
+
+#: models/calendar.py:31
+msgid "hours per day"
+msgstr "tuntia päivässä"
+
+#: models/calendar.py:32
+msgid "How many hours per day should be in this calendar"
+msgstr ""
+
+#: models/calendar.py:49
+#, python-format
+msgid ""
+"%(hours)s hours total in %(workdays)s days (%(overtime)s hours overtime)."
+msgstr ""
+"%(hours)s työtuntia %(workdays)s:ssä päivässä (%(overtime)s ylityötuntia)."
+
+#: models/calendar.py:52
+#, python-format
+msgid "%(hours)s hours total in %(workdays)s days."
+msgstr "%(hours)s työtuntia %(workdays)s:ssä päivässä."
+
+#: models/common.py:176
+msgid "New GSX Account"
+msgstr "Uusi GSX tili"
+
+#: models/common.py:177 models/queue.py:126
+msgid "Sold-To"
+msgstr "Sold-To"
+
+#: models/common.py:178 models/common.py:444
+msgid "Ship-To"
+msgstr "Ship-To"
+
+#: models/common.py:183
+msgid "Region"
+msgstr "Alue"
+
+#: models/common.py:202
+msgid "Environment"
+msgstr "Ympäristö"
+
+#: models/common.py:227
+#, fuzzy
+msgid "Default GSX account not configured"
+msgstr "Oletus GSX-tili"
+
+#: models/common.py:252
+#, fuzzy
+msgid "Configuration error"
+msgstr "Kokoonpano"
+
+#: models/common.py:297 models/escalations.py:25
+#, fuzzy
+msgid "GSX Account"
+msgstr "GSX tilit"
+
+#: models/common.py:298 templates/admin/index.html:12
+msgid "GSX Accounts"
+msgstr "GSX tilit"
+
+#: models/common.py:317 templates/admin/tags/index.html:6
+msgid "New Tag"
+msgstr "Uusi tagi"
+
+#: models/common.py:318 models/common.py:393 models/customer.py:25
+#: models/customer.py:59 models/queue.py:197
+msgid "name"
+msgstr "nimi"
+
+#: models/common.py:322 templates/devices/index.html:49
+#: templates/devices/search_gsx.html:8 templates/devices/search_gsx.html:10
+#: templates/devices/search_gsx_error.html:8 templates/devices/view.html:7
+#: templates/search/results/gsx.html:8 templates/search/results/gsx.html:10
+msgid "Device"
+msgstr "Laite"
+
+#: models/common.py:323 models/common.py:625
+#: templates/accounts/updates.html:37 templates/invoices/index.html:44
+#: templates/notes/form.html:109 templates/orders/dispatch.html:105
+#: templates/orders/edit.html:114 templates/orders/gsx_repair_form.html:89
+#: templates/orders/list.html:10 templates/orders/parts.html:33
+#: templates/orders/tabs.html:4 templates/products/index_outgoing.html:25
+#: templates/products/view.html:129 templates/products/view.html.py:155
+#: templates/products/view.html:180 templates/purchases/list_pos.html:57
+#: templates/shipments/list_returns.html:14
+msgid "Order"
+msgstr "Tilaus"
+
+#: models/common.py:324 templates/notes/form.html:37
+msgid "Note"
+msgstr "Merkintä"
+
+#: models/common.py:331 models/common.py:639
+msgid "type"
+msgstr "tyyppi"
+
+#: models/common.py:344
+msgid "Default"
+msgstr "oletus"
+
+#: models/common.py:346
+msgid "Orange"
+msgstr "Oranssi"
+
+#: models/common.py:348
+msgid "Blue"
+msgstr "Sininen"
+
+#: models/common.py:374 templates/orders/notes.html:32
+msgid "Tag"
+msgstr "Merkki"
+
+#: models/common.py:375 templates/admin/index.html:30
+#: templates/devices/form.html:25 templates/products/form.html:51
+msgid "Tags"
+msgstr "Tagit"
+
+#: models/common.py:394 templates/admin/locations/index.html:5
+msgid "New Location"
+msgstr "Uusi sijainti"
+
+#: models/common.py:400 models/customer.py:71
+msgid "phone"
+msgstr "puhelin"
+
+#: models/common.py:402 models/customer.py:76
+msgid "email"
+msgstr "Sähköposti"
+
+#: models/common.py:407 models/customer.py:82
+msgid "address"
+msgstr "Katuosoite"
+
+#: models/common.py:419 models/customer.py:94
+msgid "city"
+msgstr "toimipaikka"
+
+#: models/common.py:437
+#, fuzzy
+msgid "Accounts"
+msgstr "GSX tilit"
+
+#: models/common.py:450
+#, fuzzy
+msgid "Timezone"
+msgstr "aikavyöhyke"
+
+#: models/common.py:458
+msgid "Will be shown on print templates"
+msgstr ""
+
+#: models/common.py:465
+msgid "Logo"
+msgstr "Logo"
+
+#: models/common.py:470
+msgid "Enabled"
+msgstr "Käytössä"
+
+#: models/common.py:508 templates/admin/index.html:15
+#: templates/stats/index.html:9 templates/stats/locations.html:6
+#: templates/stats/queues.html:6 templates/stats/sales.html:6
+#: templates/stats/statuses.html:6 views/admin.py:510
+msgid "Locations"
+msgstr "Toimipisteet"
+
+#: models/common.py:624 templates/customers/view.html:7
+#: templates/devices/search_gsx_repairs.html:8
+#: templates/invoices/index.html:45 templates/orders/dispatch.html:20
+#: templates/orders/gsx_repair_form.html:44 templates/orders/list.html:11
+#: templates/products/view.html:130
+msgid "Customer"
+msgstr "Asiakas"
+
+#: models/common.py:626 templates/orders/dispatch.html:63
+#: templates/orders/print_confirmation.html:105
+#: templates/orders/print_dispatch.html:13 templates/orders/products.html:10
+#: templates/products/list.html:7 templates/products/view.html:43
+#: templates/purchases/edit_po.html:32 templates/purchases/view_po.html:31
+msgid "Product"
+msgstr "Tuote"
+
+#: models/common.py:631 templates/admin/fields/index.html:6
+msgid "New Field"
+msgstr "Uusi kenttä"
+
+#: models/common.py:645
+msgid "format"
+msgstr "muoto"
+
+#: models/common.py:647
+msgid "value"
+msgstr "arvo"
+
+#: models/common.py:664
+msgid "Field"
+msgstr "Kenttä"
+
+#: models/common.py:665 templates/admin/index.html:27
+msgid "Fields"
+msgstr "Kentät"
+
+#: models/common.py:710 templates/admin/templates/list_templates.html:6
+msgid "New Template"
+msgstr "Uusi pohja"
+
+#: models/common.py:713
+msgid "content"
+msgstr "sisältö"
+
+#: models/common.py:732
+msgid "Template"
+msgstr "Pohja"
+
+#: models/common.py:733 templates/admin/index.html:33
+#: templates/admin/queues/form.html:13
+msgid "Templates"
+msgstr "Pohjat"
+
+#: models/common.py:741
+msgid "file"
+msgstr "tiedosto"
+
+#: models/common.py:761
+#, python-format
+msgid "%s is not of an allowed file type"
+msgstr "%s ei ole sallittu tiedostomuoto"
+
+#: models/customer.py:24 templates/admin/users/index.html:12
+#: templates/customers/index.html:16
+msgid "New Group"
+msgstr "Uusi ryhmä"
+
+#: models/customer.py:54 models/customer.py:142
+msgid "company"
+msgstr "yritys"
+
+#: models/customer.py:60 templates/customers/choose.html:17
+#: templates/customers/index.html:7 templates/customers/search.html:5
+#: templates/search/spotlight.html:6
+msgid "New Customer"
+msgstr "Uusi asiakas"
+
+#: models/customer.py:114 templates/admin/users/tabs.html:6
+#: templates/customers/index.html:55
+msgid "Groups"
+msgstr "Ryhmät"
+
+#: models/customer.py:121
+msgid "tags"
+msgstr "tagit"
+
+#: models/customer.py:128 models/device.py:146
+msgid "notes"
+msgstr "merkinnät"
+
+#: models/customer.py:136
+msgid "devices"
+msgstr "laitteet"
+
+#: models/device.py:39 templates/devices/find.html:27
+#: templates/devices/get_info.html:14
+#: templates/devices/search_gsx_warranty.html:15
+#: templates/devices/summary.html:13 templates/orders/devices.html:20
+#: templates/orders/print_confirmation.html:72
+#: templates/orders/products.html:28 templates/products/index_outgoing.html:28
+#: templates/search/results/gsx_warranty.html:15
+#: templates/shipments/list_returns.html:17
+msgid "Serial Number"
+msgstr "Sarjanumero"
+
+#: models/device.py:43 templates/devices/choose.html:17
+#: templates/devices/index.html:7 templates/devices/index.html.py:10
+msgid "New Device"
+msgstr "Uusi laite"
+
+#: models/device.py:44 models/order.py:708 models/queue.py:44
+#: models/queue.py:202
+msgid "description"
+msgstr "kuvaus"
+
+#: models/device.py:49
+msgid "Apple"
+msgstr ""
+
+#: models/device.py:50 models/product.py:183
+msgid "Brand"
+msgstr "Valmistaja"
+
+#: models/device.py:57 templates/orders/print_confirmation.html:68
+msgid "IMEI Number"
+msgstr "IMEI numero"
+
+#: models/device.py:63 templates/devices/search_gsx_warranty.html:24
+#: templates/orders/devices.html:31
+msgid "Initial Activation Policy"
+msgstr "Aktivointiprofiili"
+
+#: models/device.py:69 templates/devices/search_gsx_warranty.html:22
+#: templates/orders/devices.html:29
+msgid "Applied Activation Policy"
+msgstr "Aktivointiprofiili"
+
+#: models/device.py:79 templates/devices/index.html:26
+msgid "Product Line"
+msgstr "Tuoteryhmä"
+
+#: models/device.py:85
+msgid "Products that are compatible with this device instance"
+msgstr ""
+
+#: models/device.py:92
+msgid "configuration"
+msgstr "kokoonpano"
+
+#: models/device.py:96 models/parts.py:78
+msgid "Apple Limited Warranty"
+msgstr ""
+
+#: models/device.py:97 models/parts.py:82
+msgid "AppleCare Protection Plan"
+msgstr ""
+
+#: models/device.py:98
+msgid "Customer Satisfaction (CS) Code"
+msgstr ""
+
+#: models/device.py:99 models/parts.py:75
+msgid "Custom Bid Contracts"
+msgstr ""
+
+#: models/device.py:100
+msgid "3'rd Party Warranty"
+msgstr "Kolmannen osapuolen takuu"
+
+#: models/device.py:101
+msgid "Out Of Warranty (No Coverage)"
+msgstr "Ei takuuta"
+
+#: models/device.py:102 views/gsx.py:131
+msgid "Unknown"
+msgstr "Tuntematon"
+
+#: models/device.py:109 templates/checkin/device.html:16
+#: templates/devices/find.html:30 templates/devices/get_info.html:10
+#: templates/devices/search_gsx_warranty.html:11
+#: templates/devices/summary.html:9 templates/orders/devices.html:16
+#: templates/orders/print_confirmation.html:65
+#: templates/search/results/gsx_warranty.html:11
+msgid "Warranty Status"
+msgstr "Takuu"
+
+#: models/device.py:115
+msgid "username"
+msgstr "käyttäjä"
+
+#: models/device.py:121
+msgid "password"
+msgstr "salasana"
+
+#: models/device.py:126
+msgid "Date Purchased"
+msgstr "Hankittu"
+
+#: models/device.py:134
+msgid "Purchase Country"
+msgstr "Ostomaa"
+
+#: models/device.py:158
+msgid "Image URL"
+msgstr "Kuvan osoite"
+
+#: models/device.py:163
+msgid "Manual URL"
+msgstr "Ohjeen osoite"
+
+#: models/device.py:168 templates/devices/view.html:38
+#: templates/devices/view.html.py:40
+msgid "Exploded View"
+msgstr "Avattu näkymä"
+
+#: models/device.py:285
+#, python-format
+msgid "Invalid argument for warranty check: %s"
+msgstr ""
+
+#: models/device.py:352
+#, fuzzy, python-format
+msgid "SLA Group: %s"
+msgstr "Ryhmät"
+
+#: models/device.py:354
+msgid "This unit is eligible for Onsite Service."
+msgstr ""
+
+#: models/device.py:356
+msgid "Parts and Labor are covered."
+msgstr ""
+
+#: models/escalations.py:96
+#, fuzzy, python-format
+msgid "Escalation %s"
+msgstr "Uusi sijainti"
+
+#: models/invoices.py:27 models/invoices.py:163
+msgid "No Charge"
+msgstr "Ei veloitusta"
+
+#: models/invoices.py:28 models/invoices.py:164
+msgid "Cash"
+msgstr "Käteinen"
+
+#: models/invoices.py:29 models/invoices.py:165
+#: templates/products/view.html:179
+msgid "Invoice"
+msgstr "Lasku"
+
+#: models/invoices.py:30 models/invoices.py:166
+msgid "Credit Card"
+msgstr "Maksukortti"
+
+#: models/invoices.py:31 models/invoices.py:167
+msgid "Mail payment"
+msgstr "Postiennakko"
+
+#: models/invoices.py:32 models/invoices.py:168
+msgid "Online payment"
+msgstr "Verkkomaksu"
+
+#: models/invoices.py:39 models/invoices.py:173
+#: templates/invoices/index.html:50
+msgid "Payment Method"
+msgstr "Maksutapa"
+
+#: models/invoices.py:42
+msgid "paid"
+msgstr "maksettu"
+
+#: models/invoices.py:56
+msgid "Walk-in"
+msgstr "Käteisasiakas"
+
+#: models/invoices.py:57 templates/customers/find.html:48
+#: templates/orders/print_confirmation.html:177
+msgid "Name"
+msgstr "Nimi"
+
+#: models/invoices.py:81 models/repair.py:127
+#: templates/devices/search_gsx_repairs.html:6 templates/orders/repair.html:30
+#: templates/products/receive_item.html:23
+#: templates/products/receive_item.html:30 templates/purchases/edit_po.html:33
+#: templates/purchases/list_pos.html:54 templates/purchases/view_po.html:21
+#: templates/purchases/view_po.html:32
+#: templates/shipments/edit_bulk_return.html:57
+msgid "Reference"
+msgstr "Viite"
+
+#: models/invoices.py:132
+msgid "Sales Price"
+msgstr "Myyntihinta"
+
+#: models/invoices.py:186 views/invoices.py:159
+#, python-format
+msgid "Order %s dispatched"
+msgstr "Tilaus %s toimitettu"
+
+#: models/invoices.py:195
+#, python-format
+msgid "Payment for %0.2f received"
+msgstr ""
+
+#: models/note.py:66
+msgid "subject"
+msgstr "otsikko"
+
+#: models/note.py:70
+msgid "Message"
+msgstr "Viesti"
+
+#: models/note.py:87
+msgid "To"
+msgstr "Saaja"
+
+#: models/note.py:112 models/order.py:763
+msgid "report"
+msgstr "raprtoi"
+
+#: models/note.py:116
+msgid "read"
+msgstr "Luettu"
+
+#: models/note.py:121
+msgid "flagged"
+msgstr "merkitty"
+
+#: models/note.py:159
+#, fuzzy, python-format
+msgid "Default Address <%s>"
+msgstr "Oletusosoite <%s>"
+
+#: models/note.py:271
+msgid "As Unreported"
+msgstr "Älä raportoi"
+
+#: models/note.py:271
+msgid "As Reported"
+msgstr "Raportoi"
+
+#: models/note.py:274
+msgid "As Unread"
+msgstr "Ei-luetuksi"
+
+#: models/note.py:274
+msgid "As Read"
+msgstr "Luetuksi"
+
+#: models/note.py:277
+msgid "As Unflagged"
+msgstr "Ei merkityksi"
+
+#: models/note.py:277
+msgid "As Flagged"
+msgstr "Merkityksi"
+
+#: models/note.py:319 models/note.py:409
+#, python-format
+msgid "Already sent message to %s"
+msgstr ""
+
+#: models/note.py:364 models/note.py:448
+#, fuzzy, python-format
+msgid "Message sent to %s"
+msgstr "Tekstiviesti lähtetty numeroon %s"
+
+#: models/note.py:385
+msgid "System is not configured for built-in SMS support."
+msgstr ""
+
+#: models/note.py:417
+msgid "SMS gateway not configured"
+msgstr ""
+
+#: models/note.py:473
+#, fuzzy, python-format
+msgid "Escalation %s created"
+msgstr "Uusi sijainti"
+
+#: models/note.py:476
+#, fuzzy, python-format
+msgid "Escalation %s updated"
+msgstr "Uusi sijainti"
+
+#: models/note.py:481
+msgid "Note saved"
+msgstr "Merkintä tallennettu"
+
+#: models/order.py:41 models/queue.py:60
+msgid "priority"
+msgstr "prioriteetti"
+
+#: models/order.py:106 models/repair.py:39
+msgid "queue"
+msgstr "jono"
+
+#: models/order.py:112
+msgid "status"
+msgstr "status"
+
+#: models/order.py:129
+msgid "Closed"
+msgstr "Suljettu"
+
+#: models/order.py:215
+#, python-format
+msgid "%s added"
+msgstr "%s lisätty"
+
+#: models/order.py:219
+msgid "This device has already been added to this order"
+msgstr "Tämä laite on jo tässä tilauksessa"
+
+#: models/order.py:237
+#, python-format
+msgid "%s removed"
+msgstr "%s poistettu"
+
+#: models/order.py:260
+msgid "Closed a moment ago"
+msgstr "Suljettu hetki sitten"
+
+#: models/order.py:261
+#, python-format
+msgid "Closed for %(time)s"
+msgstr "Suljettu %(time)s"
+
+#: models/order.py:265
+#, python-format
+msgid "%s a moment ago"
+msgstr "%s hetki sitten"
+
+#: models/order.py:268
+#, python-format
+msgid "%(status)s for %(time)s"
+msgstr "%(status)s %(time)s"
+
+#: models/order.py:272
+msgid "Created a moment ago"
+msgstr "Luotu hetki sitten"
+
+#: models/order.py:273
+#, python-format
+msgid "Unassigned for %(delta)s"
+msgstr "Odottava %(delta)s"
+
+#: models/order.py:277
+msgid "Started a moment ago"
+msgstr "Aloitettu hetki sitten"
+
+#: models/order.py:278
+#, python-format
+msgid "Open for %(delta)s"
+msgstr "Auki %(delta)s"
+
+#: models/order.py:281
+msgid "Select place"
+msgstr "Valitse sijainti"
+
+#: models/order.py:284
+msgid "Select status"
+msgstr "Valitse status"
+
+#: models/order.py:291
+msgid "Select user"
+msgstr "Valitse käyttäjä"
+
+#: models/order.py:294
+msgid "Select queue"
+msgstr "Valitse jono"
+
+#: models/order.py:303 templates/default.html:35
+#: templates/accounts/orders.html:58 templates/accounts/tabs.html:7
+#: templates/customers/view.html:8 templates/devices/view.html:8
+#: templates/orders/index.html:49 templates/search/spotlight.html:22
+#: views/order.py:43
+msgid "Orders"
+msgstr "Tilaukset"
+
+#: models/order.py:312 views/order.py:195
+#, python-format
+msgid "Order %s closed"
+msgstr "Tilaus %s suljettu"
+
+#: models/order.py:411
+#, fuzzy
+msgid "Order must belong to a queue to set status"
+msgstr "Tilaus on määrätty johonkin jonoon"
+
+#: models/order.py:445
+msgid "Status unassigned"
+msgstr "Status poistettu"
+
+#: models/order.py:475
+msgid "Removed from queue"
+msgstr "Poista jonosta"
+
+#: models/order.py:501
+msgid "Order unassigned"
+msgstr "Käsittelijä poistettu"
+
+#: models/order.py:505
+#, python-format
+msgid "Order %(order)s assigned to %(user)s"
+msgstr "%(user)s vaihdettu tilauksen %(order)s käsittelijäksi"
+
+#: models/order.py:598 views/order.py:693 views/purchases.py:90
+#, python-format
+msgid "Product %s added"
+msgstr "Tuote %s lisätty"
+
+#: models/order.py:604
+#, fuzzy, python-format
+msgid "Product %s removed from order"
+msgstr "Tuote %s poistettu"
+
+#: models/order.py:685
+msgid "Can set assignee"
+msgstr "Voi vaihtaa käsittelijää"
+
+#: models/order.py:686
+msgid "Can change status"
+msgstr "Voi muuttaa statusta"
+
+#: models/order.py:687
+msgid "Can follow order"
+msgstr "Voi seurata tilausta"
+
+#: models/order.py:719
+msgid "amount"
+msgstr "määrä"
+
+#: models/order.py:724
+msgid "KGB Serial Number"
+msgstr "KGB sarjanumero"
+
+#: models/order.py:759
+msgid "dispatched"
+msgstr "toimitettu"
+
+#: models/order.py:768
+msgid "sales price"
+msgstr "Myyntihinta"
+
+#: models/order.py:783
+msgid "KBB Serial Number"
+msgstr "KBB sarjanumero"
+
+#: models/order.py:790 templates/orders/print_confirmation.html:120
+#: templates/orders/print_receipt.html:23
+msgid "IMEI"
+msgstr ""
+
+#: models/order.py:794 templates/orders/print_confirmation.html:106
+#: templates/orders/print_dispatch.html:14
+msgid "Warranty"
+msgstr "Takuu"
+
+#: models/order.py:795 templates/devices/parts.html:11
+#: templates/products/form.html:11 templates/products/get_info.html:23
+#: templates/products/list.html:9 templates/products/view.html:64
+msgid "Exchange Price"
+msgstr "Vaihtohinta"
+
+#: models/order.py:796 templates/devices/parts.html:10
+#: templates/products/form.html:12 templates/products/get_info.html:21
+#: templates/products/list.html:8 templates/products/view.html:62
+msgid "Stock Price"
+msgstr "Myyntihinta"
+
+#: models/order.py:803
+msgid "Price category"
+msgstr "Hintaluokka"
+
+#: models/order.py:810
+msgid "symptom code"
+msgstr "Oirekoodi"
+
+#: models/order.py:816
+msgid "symptom modifier"
+msgstr "Toistuvuus"
+
+#: models/order.py:1005 models/product.py:589
+#, fuzzy
+msgid "New Shipping Method"
+msgstr "Kuljetuskulut"
+
+#: models/order.py:1034 views/order.py:238
+#, python-format
+msgid "Order %s created"
+msgstr "Tilaus %s luotu"
+
+#: models/parts.py:76
+#, fuzzy
+msgid "Customer Satisfaction"
+msgstr "Asiakastiedot"
+
+#: models/parts.py:77
+#, fuzzy
+msgid "DOA Coverage"
+msgstr "Takuustatus"
+
+#: models/parts.py:79
+msgid "Missing Upon First Use"
+msgstr ""
+
+#: models/parts.py:80
+#, fuzzy
+msgid "Out of Warranty (no coverage)"
+msgstr "Ei takuuta"
+
+#: models/parts.py:81
+msgid "AppleCare Parts Agreement"
+msgstr ""
+
+#: models/parts.py:83
+msgid "Quality Program"
+msgstr ""
+
+#: models/parts.py:84
+msgid "AppleCare Repair Agreement"
+msgstr ""
+
+#: models/parts.py:85 templates/orders/devices.html:13
+#, fuzzy
+msgid "Repeat Service"
+msgstr "Palvelu"
+
+#: models/parts.py:86
+#, fuzzy
+msgid "Additional Part Coverage"
+msgstr "Takuustatus"
+
+#: models/parts.py:87
+msgid "Additional Service Coverage"
+msgstr ""
+
+#: models/parts.py:88
+msgid "NEW - AppleCare Protection Plan"
+msgstr ""
+
+#: models/parts.py:89
+#, fuzzy
+msgid "Consumer Law Coverage"
+msgstr "Takuustatus"
+
+#: models/parts.py:231
+msgid "Unregister from Return"
+msgstr "Poista palautuslistalta"
+
+#: models/parts.py:233 templates/repairs/part_menu.html:8
+msgid "Register for Return"
+msgstr "Lisää palautuslistalle"
+
+#: models/parts.py:328
+msgid "GSX repair has no dispatch ID"
+msgstr "GSX korjauksella ei ole vahvistusnumeroa"
+
+#: models/product.py:64
+msgid "code"
+msgstr "koodi"
+
+#: models/product.py:71
+msgid "Substituted (new) code of this part"
+msgstr ""
+
+#: models/product.py:76 templates/products/index.html:9
+msgid "New Product"
+msgstr "Uusi tuote"
+
+#: models/product.py:77 models/queue.py:27
+#: templates/products/index_outgoing.html:24
+#: templates/shipments/list_returns.html:13
+msgid "Title"
+msgstr "Nimike"
+
+#: models/product.py:82 models/repair.py:64 templates/devices/find.html:28
+msgid "Description"
+msgstr "Kuvaus"
+
+#: models/product.py:93
+msgid "Don't update price when recalculating prices or importing parts"
+msgstr ""
+
+#: models/product.py:100 models/product.py:128
+msgid "Purchase price"
+msgstr "Ostohinta"
+
+#: models/product.py:113 models/product.py:140
+msgid "Net price"
+msgstr "Veroton hinta"
+
+#: models/product.py:114 models/product.py:141
+msgid "Purchase price + margin %"
+msgstr "Ostohinta + kate %"
+
+#: models/product.py:120 models/product.py:147
+msgid "Sales price"
+msgstr "Myyntihinta"
+
+#: models/product.py:121 models/product.py:148
+msgid "Purchase price + margin % + shipping + VAT %"
+msgstr "Ostohinta + kate % + toimituskulut + ALV %"
+
+#: models/product.py:153
+msgid "is serialized"
+msgstr "sarjanumeroseuranta"
+
+#: models/product.py:154
+msgid "Product has a serial number"
+msgstr "Tuotteessa käytetään sarjanumeroa"
+
+#: models/product.py:171
+msgid "Warranty (months)"
+msgstr "takuu (kk)"
+
+#: models/product.py:177
+msgid "Shelf code"
+msgstr "Hyllykoodi"
+
+#: models/product.py:189 templates/admin/inventory/index.html:9
+#: templates/products/index.html:60 templates/products/view.html:10
+msgid "Categories"
+msgstr "Tuoteryhmät"
+
+#: models/product.py:195
+msgid "device models"
+msgstr "laitemallit"
+
+#: models/product.py:205
+msgid "shipping"
+msgstr "toimituskulut"
+
+#: models/product.py:213
+msgid "component group"
+msgstr "Komponenttiryhmä"
+
+#: models/product.py:220
+msgid "Module"
+msgstr "Moduuli"
+
+#: models/product.py:221
+msgid "Replacement"
+msgstr "Vaihtolaite"
+
+#: models/product.py:222
+msgid "Service"
+msgstr "Palvelu"
+
+#: models/product.py:223
+msgid "Service Contract"
+msgstr "Huoltosopimus"
+
+#: models/product.py:231
+msgid "part type"
+msgstr "osatyyppi"
+
+#: models/product.py:238
+msgid "EEE code"
+msgstr "EEE-koodi"
+
+#: models/product.py:252
+#, python-format
+msgid "%d months"
+msgstr "%d kk"
+
+#: models/product.py:419
+#, python-format
+msgid "Product %s not found in inventory."
+msgstr "Tuotteella %s ei ole varastopaikkaa"
+
+#: models/product.py:495
+msgid "Can change product amount"
+msgstr "Saa muuttaa varastosaldoja"
+
+#: models/product.py:507 templates/products/index.html:12
+msgid "New Category"
+msgstr "Uusi tuoteryhmä"
+
+#: models/product.py:557
+msgid "minimum amount"
+msgstr "minimimäärä"
+
+#: models/product.py:561
+msgid "reserved amount"
+msgstr "varattu"
+
+#: models/product.py:565
+msgid "stocked amount"
+msgstr "vapaana"
+
+#: models/product.py:569
+msgid "ordered amount"
+msgstr "tilattu"
+
+#: models/purchases.py:35
+msgid "The location from which this PO was created"
+msgstr ""
+
+#: models/purchases.py:42
+msgid "reference"
+msgstr "viite"
+
+#: models/purchases.py:48
+msgid "confirmation"
+msgstr "vahvistus"
+
+#: models/purchases.py:58
+msgid "supplier"
+msgstr "toimittaja"
+
+#: models/purchases.py:63 models/shipments.py:65
+msgid "carrier"
+msgstr "toimittaja"
+
+#: models/purchases.py:68
+msgid "tracking ID"
+msgstr "seurantakoodi"
+
+#: models/purchases.py:73
+msgid "delivery Time"
+msgstr "toimitusaika"
+
+#: models/purchases.py:125
+#, python-format
+msgid "Purchase Order %d has already been submitted"
+msgstr "Ostotilaus %d on jo vahvistettu"
+
+#: models/purchases.py:178
+msgid "Submitted orders cannot be deleted"
+msgstr "Lähetettyjä tiluaksia ei voi enää muokata"
+
+#: models/purchases.py:191 templates/products/receive_item.html:36
+#: templates/purchases/edit_po.html:35 templates/purchases/view_po.html:34
+msgid "Purchase Price"
+msgstr "Ostohinta"
+
+#: models/purchases.py:192
+msgid "Purchase price without taxes"
+msgstr "Ostohinta ilman ALV"
+
+#: models/purchases.py:198
+msgid "Purchase Order"
+msgstr "Ostotilaus"
+
+#: models/purchases.py:210
+msgid "arrived"
+msgstr "saapunut"
+
+#: models/purchases.py:248
+msgid "Product has already been received"
+msgstr "Tuote %s on jo saavutettu"
+
+#: models/purchases.py:291
+#, fuzzy, python-format
+msgid "Cannot receive item %(prod)s (%(ref)s)"
+msgstr "Ei voida poistaa tilausta %s: %s"
+
+#: models/purchases.py:318 views/purchases.py:215
+#, python-format
+msgid "Purchase Order %d created"
+msgstr "Ostotilaus %d luotu"
+
+#: models/queue.py:26 templates/admin/queues/index.html:5
+msgid "New Queue"
+msgstr "Uusi jono"
+
+#: models/queue.py:33
+msgid ""
+"Orders with devices matching these keywords will be automatically assigned "
+"to this queue"
+msgstr ""
+
+#: models/queue.py:38
+msgid "locations"
+msgstr "Toimipaikat"
+
+#: models/queue.py:39
+msgid "Pick the locations you want this queue to appear in."
+msgstr "Valitse toimipaikat jossa haluat käyttää tätä jonoa"
+
+#: models/queue.py:52
+msgid "High"
+msgstr "Korkea"
+
+#: models/queue.py:53
+msgid "Normal"
+msgstr "Normaali"
+
+#: models/queue.py:54
+msgid "Low"
+msgstr "Matala"
+
+#: models/queue.py:68
+msgid "Order Created"
+msgstr "Tilaus %s luotu"
+
+#: models/queue.py:69
+msgid "Order has ben placed to a queue"
+msgstr "Tilaus on määrätty johonkin jonoon"
+
+#: models/queue.py:77
+msgid "Order Assigned"
+msgstr "Käsittelijä vaihdettu"
+
+#: models/queue.py:78
+msgid "Order has ben assigned to a user"
+msgstr "Käsittelijä vaihdettu"
+
+#: models/queue.py:86
+msgid "Products Ordered"
+msgstr "Tuotteet tilattu"
+
+#: models/queue.py:87
+msgid "Purchase Order for this Service Order has been submitted"
+msgstr "Huoltotilauksen ostotilaus on lähetetty"
+
+#: models/queue.py:94
+msgid "Products Received"
+msgstr "Tuotteet saavutettu"
+
+#: models/queue.py:95
+msgid "Products have been received"
+msgstr "Tuotteet on saavutettu"
+
+#: models/queue.py:102
+msgid "Repair Completed"
+msgstr "Korjaus merkitty valmiiksi"
+
+#: models/queue.py:103
+msgid "GSX repair completed"
+msgstr "GSX korjaus %s suljettu"
+
+#: models/queue.py:111
+msgid "Order Dispatched"
+msgstr "Tilaus toimitettu"
+
+#: models/queue.py:119
+msgid "Order Closed"
+msgstr "Tilaus suljettu"
+
+#: models/queue.py:127
+msgid "GSX queries of an order in this queue will be made using this Sold-To"
+msgstr ""
+
+#: models/queue.py:134
+msgid "order template"
+msgstr "Tilauspohja"
+
+#: models/queue.py:135
+msgid "HTML template for Service Order/Work Confirmation"
+msgstr ""
+
+#: models/queue.py:141
+msgid "quote template"
+msgstr "Tilauspohja"
+
+#: models/queue.py:142
+msgid "HTML template for cost estimate"
+msgstr "HTML pohja kustannusarvioita varten"
+
+#: models/queue.py:148
+msgid "receipt template"
+msgstr "Kuittipohja"
+
+#: models/queue.py:149
+msgid "HTML template for Sales Order Receipt"
+msgstr "HTML pohja myyntitilausta varten"
+
+#: models/queue.py:155
+msgid "dispatch template"
+msgstr "Lähetepohja"
+
+#: models/queue.py:156
+msgid "HTML template for dispatched order"
+msgstr ""
+
+#: models/queue.py:175 templates/admin/index.html:21
+#: templates/stats/index.html:10 templates/stats/locations.html:7
+#: templates/stats/queues.html:7 templates/stats/sales.html:7
+#: templates/stats/statuses.html:7 views/admin.py:577
+msgid "Queues"
+msgstr "Jonot"
+
+#: models/queue.py:187
+msgid "Minutes"
+msgstr "Minuuttia"
+
+#: models/queue.py:188
+msgid "Hours"
+msgstr "Tuntia"
+
+#: models/queue.py:189
+msgid "Days"
+msgstr "Päivää"
+
+#: models/queue.py:190
+msgid "Weeks"
+msgstr "Viikkoa"
+
+#: models/queue.py:191
+msgid "Months"
+msgstr "Kuukautta"
+
+#: models/queue.py:196 templates/admin/statuses/index.html:5
+msgid "New Status"
+msgstr "Uusi status"
+
+#: models/queue.py:206 models/queue.py:249
+msgid "green limit"
+msgstr "virhreä raja"
+
+#: models/queue.py:210 models/queue.py:250
+msgid "yellow limit"
+msgstr "keltainen raja"
+
+#: models/queue.py:215 models/queue.py:253
+msgid "time unit"
+msgstr "aikayksikkö"
+
+#: models/queue.py:237 templates/accounts/updates.html:28
+#: templates/admin/index.html:18 templates/admin/queues/form.html:10
+#: templates/stats/index.html:11 templates/stats/locations.html:8
+#: templates/stats/queues.html:8 templates/stats/sales.html:8
+#: templates/stats/statuses.html:8 views/admin.py:261
+msgid "Statuses"
+msgstr "Statukset"
+
+#: models/repair.py:33 templates/admin/checklist/index.html:5
+#, fuzzy
+msgid "New Checklist"
+msgstr "Muistilistat"
+
+#: models/repair.py:42
+msgid "enabled"
+msgstr "käytössä"
+
+#: models/repair.py:55 templates/admin/index.html:36
+msgid "Checklists"
+msgstr "Muistilistat"
+
+#: models/repair.py:60
+msgid "Task"
+msgstr "Tehtävä"
+
+#: models/repair.py:107
+msgid "Unit Received"
+msgstr "Vastaanotettu"
+
+#: models/repair.py:120
+msgid "Repair should be reviewed by Apple before confirmation"
+msgstr ""
+
+#: models/repair.py:135
+msgid "Notes are mandatory when requesting review."
+msgstr ""
+
+#: models/repair.py:142
+msgid "Choose files to be sent with the repair creation request"
+msgstr ""
+
+#: models/repair.py:148
+msgid "mark complete"
+msgstr "merkitse valmiiksi"
+
+#: models/repair.py:149
+msgid "Requires replacement serial number"
+msgstr ""
+
+#: models/repair.py:155
+#, fuzzy
+msgid "New serial number"
+msgstr "Sarjanumero"
+
+#: models/repair.py:156
+msgid "Serial Number of replacement part"
+msgstr "Vaihto-osan sarjanumero"
+
+#: models/repair.py:175
+msgid "Unit is eligible for consumer law coverage"
+msgstr ""
+
+#: models/repair.py:223
+msgid "New GSX Repair"
+msgstr "Uusi GSX korjaus"
+
+#: models/repair.py:278
+msgid "Please add some parts to the repair"
+msgstr "Tilaus ei sisällä sopivia osia"
+
+#: models/repair.py:281
+msgid "Order has not been assigned to a queue"
+msgstr "Tilausta ei ole laitettu mihinkään jonoon"
+
+#: models/repair.py:327 views/gsx.py:193
+#, python-format
+msgid "GSX repair %s created"
+msgstr "GSX korjaus %s luotu"
+
+#: models/repair.py:582
+#, python-format
+msgid "Repair %d"
+msgstr "Huolto %d"
+
+#: models/shipments.py:43 templates/shipments/list_bulk_returns.html:10
+msgid "Tracking ID"
+msgstr "seurantakoodi"
+
+#: models/shipments.py:90
+msgid "width"
+msgstr "leveys"
+
+#: models/shipments.py:96
+msgid "height"
+msgstr "korkeus"
+
+#: models/shipments.py:102
+msgid "length"
+msgstr "pituus"
+
+#: models/shipments.py:108
+msgid "weight"
+msgstr "paino"
+
+#: templates/default.html:34 templates/default.html.py:66
+msgid "Homepage"
+msgstr "Kotisivu"
+
+#: templates/default.html:36
+msgid "Inventory"
+msgstr "Varasto"
+
+#: templates/default.html:37 templates/customers/view.html:9
+#: templates/devices/index.html:77 templates/devices/search.html:14
+#: templates/search/spotlight.html:16 views/device.py:48
+msgid "Devices"
+msgstr "Laitteet"
+
+#: templates/default.html:38 templates/customers/index.html:82
+#: templates/customers/list.html:5 templates/customers/search.html:33
+#: templates/customers/view.html:117 templates/search/spotlight.html:13
+#: views/customer.py:27
+msgid "Customers"
+msgstr "Asiakkaat"
+
+#: templates/default.html:39 templates/accounts/updates.html:27
+#: templates/customers/view.html:10 templates/notes/list_notes.html:36
+#: views/note.py:54
+msgid "Messages"
+msgstr "Viestit"
+
+#: templates/default.html:40 templates/accounts/tabs.html:16
+#: templates/stats/index.html:58 views/stats.py:296
+msgid "Statistics"
+msgstr "Tilastot"
+
+#: templates/default.html:44 templates/accounts/orders.html:44
+#: templates/accounts/stats.html:19 templates/customers/choose.html:18
+#: templates/customers/find.html:16 templates/customers/find.html.py:31
+#: templates/customers/index.html:44 templates/devices/choose.html:18
+#: templates/devices/find.html:8 templates/devices/find.html.py:21
+#: templates/devices/index.html:20 templates/invoices/index.html:31
+#: templates/notes/find.html:9 templates/notes/find.html.py:25
+#: templates/notes/list_notes.html:29 templates/orders/index.html:36
+#: templates/products/choose.html:18 templates/products/index.html:84
+#: templates/purchases/list_pos.html:37
+#: templates/snippets/filtering_form.html:12 templates/stats/index.html:30
+msgid "Search"
+msgstr "Hae"
+
+#: templates/default.html:67 templates/accounts/calendars.html:36
+#: templates/accounts/calendars.html:54 templates/accounts/tabs.html:13
+#: templates/accounts/view_calendar.html:64 views/account.py:128
+msgid "Calendars"
+msgstr "Kalenterit"
+
+#: templates/default.html:68 templates/accounts/settings.html:11
+#: templates/admin/users/form.html:11
+msgid "Profile"
+msgstr "Profiili"
+
+#: templates/default.html:70 templates/admin/index.html:7 views/admin.py:205
+msgid "System Settings"
+msgstr "Järjestelmäasetukset"
+
+#: templates/default.html:73 templates/accounts/logout.html:15
+#: templates/checkin/index.html:47
+msgid "Log out"
+msgstr "Kirjaudu ulos"
+
+#: templates/default.html:100
+#, fuzzy
+msgid "No new alerts"
+msgstr "Ei uusia ilmoituksia"
+
+#: templates/default.html:105
+msgid "See All Alerts"
+msgstr ""
+
+#: templates/default.html:133
+#, fuzzy
+msgid "No new messages"
+msgstr "Tekstiviestit"
+
+#: templates/default.html:139
+#, fuzzy
+msgid "Read All Messages"
+msgstr "Tekstiviestit"
+
+#: templates/default.html:168
+msgid "Terms"
+msgstr ""
+
+#: templates/error.html:12
+#, fuzzy
+msgid "An Error Occurred"
+msgstr "Tapahtui virhe"
+
+#: templates/error.html:27 templates/checkin/error.html:9
+msgid "Oops!"
+msgstr ""
+
+#: templates/error.html:28 templates/checkin/error.html:10
+msgid "It appears that an error has occurred."
+msgstr ""
+
+#: templates/error.html:28
+msgid ""
+"I've notified the developers, but I'm sure they would really appreciate if "
+"you could briefly describe what you were doing before this happened."
+msgstr ""
+
+#: templates/error.html:33 templates/orders/dispatch.html:96
+#: templates/products/form.html:74 templates/products/form.html.py:76
+#: templates/products/receive_item.html:47
+msgid "Cancel"
+msgstr "Peruuta"
+
+#: templates/error.html:34 templates/accounts/register.html:11
+#: templates/checkin/confirmation.html:11 templates/checkin/customer.html:62
+#: templates/notes/edit_escalation.html:13
+#: templates/orders/gsx_repair_form.html:79
+#: templates/products/index_outgoing.html:49
+#: templates/purchases/edit_po.html:87 templates/purchases/order_stock.html:15
+#: templates/shipments/edit_bulk_return.html:96
+#: templates/shipments/list_returns.html:38
+#: templates/shipments/submit_bulk_return.html:13
+msgid "Submit"
+msgstr "Lähetä"
+
+#: templates/error.html:38
+msgid "Thanks!"
+msgstr ""
+
+#: templates/error.html:39
+msgid ""
+"Your error report has been submitted. Thanks for helping make Servo better!"
+msgstr ""
+
+#: templates/error.html:40
+#, fuzzy
+msgid "Return Home"
+msgstr "Palautustilaus"
+
+#: templates/form_buttons.html:3 templates/accounts/register.html:10
+#: templates/admin/users/form.html:50 templates/checkin/choose_model.html:137
+#: templates/checkin/customer.html:61 templates/checkin/device.html:56
+#: templates/checkin/issue.html:19 templates/checkin/sn.html:14
+#: templates/checkin/status.html:14
+msgid "Back"
+msgstr "Peruuta"
+
+#: templates/form_buttons.html:4 templates/admin/settings.html:110
+#: templates/admin/checklist/form.html:28 templates/admin/fields/form.html:14
+#: templates/admin/gsx/form.html:17 templates/admin/locations/form.html:37
+#: templates/admin/notifications/index.html:36
+#: templates/admin/statuses/form.html:18 templates/admin/tags/form.html:14
+#: templates/admin/templates/form.html:18 templates/admin/users/form.html:51
+#: templates/notes/form.html:101 templates/orders/gsx_repair_form.html:79
+#: templates/products/form.html:78 templates/purchases/edit_po.html:87
+#: templates/shipments/edit_bulk_return.html:96
+msgid "Save"
+msgstr "Tallenna"
+
+#: templates/modal.html:10 templates/admin/fields/remove.html:7
+#: templates/admin/statuses/remove.html:9 templates/admin/users/remove.html:9
+msgid "This action cannot be undone."
+msgstr "Toimintoa ei voi peruuttaa."
+
+#: templates/modal.html:15 templates/orders/parts.html:20
+#: templates/products/get_info.html:58 templates/snippets/modal.html:12
+msgid "Done"
+msgstr "Valmis"
+
+#: templates/pagination.html:11
+msgid "Page"
+msgstr "Sivu"
+
+#: templates/accounts/calendars.html:9
+msgid "New Event"
+msgstr "Uusi tapahtuma"
+
+#: templates/accounts/calendars.html:15 templates/checkin/thanks.html:13
+#: templates/orders/toolbar.html:20
+msgid "Print"
+msgstr "Tulosta"
+
+#: templates/accounts/calendars.html:16 templates/customers/find.html:75
+#: templates/customers/find.html.py:77 templates/customers/view.html:85
+#: templates/customers/view.html.py:87 templates/devices/find.html:54
+#: templates/devices/list.html:27 templates/devices/view.html:60
+#: templates/generic/index.html:15 templates/orders/customer.html:20
+#: templates/orders/devices.html:119 templates/orders/products.html:67
+#: templates/products/get_info.html:56 templates/products/view.html:119
+#: templates/products/view.html.py:121 templates/purchases/list_pos.html:92
+#: templates/purchases/list_pos.html:94
+msgid "Edit"
+msgstr "Muokkaa"
+
+#: templates/accounts/calendars.html:18 templates/accounts/calendars.html:20
+#: templates/accounts/delete_calendar.html:15
+#: templates/accounts/delete_calendar_event.html:11
+#: templates/admin/checklist/form.html:24
+#: templates/admin/checklist/form.html:26 templates/admin/fields/form.html:10
+#: templates/admin/fields/form.html:12 templates/admin/gsx/form.html:13
+#: templates/admin/gsx/form.html.py:15 templates/admin/gsx/remove.html:11
+#: templates/admin/locations/form.html:33
+#: templates/admin/locations/form.html:35 templates/admin/queues/form.html:29
+#: templates/admin/queues/remove.html:15 templates/admin/statuses/form.html:14
+#: templates/admin/statuses/form.html:16
+#: templates/admin/statuses/remove.html:15 templates/admin/tags/form.html:10
+#: templates/admin/tags/form.html.py:12 templates/admin/templates/form.html:14
+#: templates/admin/templates/form.html:16
+#: templates/admin/users/delete_group.html:9
+#: templates/admin/users/form.html:46 templates/admin/users/form.html.py:48
+#: templates/admin/users/remove.html:15
+#: templates/customers/delete_group.html:15 templates/customers/find.html:81
+#: templates/customers/find.html.py:83 templates/customers/remove.html:5
+#: templates/customers/remove.html.py:15 templates/customers/view.html:77
+#: templates/customers/view.html.py:79 templates/devices/find.html:56
+#: templates/devices/list.html:29 templates/devices/remove.html:15
+#: templates/devices/view.html:52 templates/devices/view.html.py:54
+#: templates/generic/delete.html:15 templates/generic/index.html:16
+#: templates/notes/form.html:99 templates/notes/remove.html:15
+#: templates/notes/view_note.html:34 templates/orders/delete_order.html:15
+#: templates/orders/gsx_repair_form.html:77 templates/orders/notes.html:50
+#: templates/orders/products.html:77 templates/orders/remove_product.html:15
+#: templates/products/delete_category.html:15
+#: templates/products/list_rows.html:44 templates/products/list_rows.html:46
+#: templates/products/remove.html:11 templates/products/view.html:109
+#: templates/products/view.html.py:113 templates/purchases/edit_po.html:37
+#: templates/purchases/list_pos.html:97 templates/purchases/list_pos.html:99
+#: templates/repairs/delete_part.html:10
+#: templates/repairs/delete_repair.html:14
+msgid "Delete"
+msgstr "Poista"
+
+#: templates/accounts/calendars.html:26 templates/customers/find.html:7
+#: templates/customers/find.html.py:9
+msgid "Download"
+msgstr "Lataa"
+
+#: templates/accounts/calendars.html:47
+msgid "No calendar selected"
+msgstr "Valitse kalenteri"
+
+#: templates/accounts/delete_calendar.html:9
+msgid "This will also delete all events in this calendar"
+msgstr "Tämä poistaa myös kaikki kalenterin tapahtumat."
+
+#: templates/accounts/login.html:18
+msgid "Login"
+msgstr "Kirjaudu"
+
+#: templates/accounts/logout.html:5
+#, fuzzy
+msgid "Logging out?"
+msgstr "Kirjaudu ulos"
+
+#: templates/accounts/logout.html:9
+msgid "This will terminate your Servo session."
+msgstr ""
+
+#: templates/accounts/orders.html:7 templates/checkin/welcome.html:12
+#: templates/notes/view_note.html:33 templates/orders/customer.html:18
+#: templates/orders/index.html:7
+msgid "Create Order"
+msgstr "Luo tilaus"
+
+#: templates/accounts/orders.html:18 templates/devices/search.html:6
+#: templates/orders/index.html:18 templates/orders/parts.html:19
+#: templates/products/index.html:49 templates/products/search.html:5
+#: templates/shipments/index.html:12
+msgid "Filter results"
+msgstr "Suodata tuloksia"
+
+#: templates/accounts/orders.html:43 templates/orders/index.html:35
+msgid "Reset"
+msgstr "Nollaa"
+
+#: templates/accounts/print_calendar.html:29
+#: templates/accounts/print_calendar.html:79
+msgid "Employee"
+msgstr "Työntekijä"
+
+#: templates/accounts/print_calendar.html:30
+#: templates/accounts/print_calendar.html:41
+#: templates/accounts/view_calendar.html:28
+#: templates/orders/print_confirmation.html:33
+#: templates/orders/print_confirmation.html:162
+#: templates/orders/print_confirmation.html:179
+msgid "Date"
+msgstr "Päivämäärä"
+
+#: templates/accounts/print_calendar.html:42
+#: templates/accounts/view_calendar.html:29
+msgid "Started At"
+msgstr "Aloitettu"
+
+#: templates/accounts/print_calendar.html:43
+#: templates/accounts/view_calendar.html:30
+msgid "Finished At"
+msgstr "Lopetettu"
+
+#: templates/accounts/print_calendar.html:44
+#: templates/accounts/view_calendar.html:31
+msgid "Duration"
+msgstr "Kesto"
+
+#: templates/accounts/print_calendar.html:58
+#: templates/accounts/view_calendar.html:57
+msgid "No events found"
+msgstr "Ei yhtään tapahtumaa"
+
+#: templates/accounts/print_calendar.html:63 templates/invoices/index.html:48
+#: templates/notes/form.html:25 templates/orders/dispatch.html:67
+#: templates/orders/dispatch.html.py:83
+#: templates/orders/gsx_repair_form.html:34
+#: templates/orders/list_products.html:20
+#: templates/orders/print_confirmation.html:111
+#: templates/orders/print_confirmation.html:139
+#: templates/orders/products.html:14 templates/purchases/edit_po.html:67
+#: templates/purchases/list_pos.html:59
+msgid "Total"
+msgstr "Yhteensä"
+
+#: templates/accounts/print_calendar.html:75
+msgid "Manager"
+msgstr "Esimies"
+
+#: templates/accounts/register.html:6 views/account.py:326
+msgid "Register"
+msgstr "Rekisteröidy"
+
+#: templates/accounts/settings.html:12 templates/admin/locations/form.html:7
+#: templates/admin/queues/form.html:12 templates/admin/users/form.html:12
+#: templates/products/form.html:15 templates/search/spotlight.html:19
+msgid "GSX"
+msgstr "GSX"
+
+#: templates/accounts/settings.html:39 templates/accounts/tabs.html:19
+#: templates/admin/index.html:9
+msgid "Settings"
+msgstr "Asetukset"
+
+#: templates/accounts/stats.html:28
+msgid "Runrate"
+msgstr ""
+
+#: templates/accounts/stats.html:29
+msgid ""
+"Shows you how many you've been assigned to during the selected period, "
+"averaged over the time scale."
+msgstr ""
+
+#: templates/accounts/tabs.html:10 templates/accounts/updates.html:26
+#: templates/accounts/updates.html.py:64 views/account.py:401
+#, fuzzy
+msgid "Updates"
+msgstr "Päivitä hinnat"
+
+#: templates/accounts/updates.html:7
+msgid "Clear all"
+msgstr "Kuittaa kaikki"
+
+#: templates/accounts/updates.html:36
+#, fuzzy
+msgid "Event"
+msgstr "Tapahtumat"
+
+#: templates/accounts/view_calendar.html:10 views/stats.py:243
+msgid "Day"
+msgstr "Päivä"
+
+#: templates/accounts/view_calendar.html:11 views/stats.py:244
+msgid "Week"
+msgstr "Viikko"
+
+#: templates/accounts/view_calendar.html:12 views/stats.py:245
+msgid "Month"
+msgstr "Kuukausi"
+
+#: templates/accounts/view_calendar.html:19
+msgid "Today"
+msgstr "Tänään"
+
+#: templates/admin/index.html:46
+msgid "Systen Settings"
+msgstr "Järjestelmäasetukset"
+
+#: templates/admin/settings.html:11 templates/admin/checklist/form.html:9
+#: templates/admin/gsx/form.html:6 templates/admin/locations/form.html:6
+#: templates/admin/queues/form.html:9 templates/admin/statuses/form.html:6
+#: templates/admin/tags/index.html:31 templates/admin/templates/form.html:6
+#: templates/admin/users/form.html:8 templates/customers/form.html:8
+#: templates/devices/form.html:8 templates/products/form.html:10
+msgid "General"
+msgstr "Yleiset"
+
+#: templates/admin/settings.html:12 templates/admin/inventory/index.html:8
+#: templates/products/form.html:13
+msgid "Stock"
+msgstr "Saldot"
+
+#: templates/admin/settings.html:13
+msgid "Outgoing Mail"
+msgstr "Lähtevä posti"
+
+#: templates/admin/settings.html:14
+msgid "Incoming Mail"
+msgstr "Tuleva posti"
+
+#: templates/admin/settings.html:15
+msgid "Text Messages"
+msgstr "Tekstiviestit"
+
+#: templates/admin/settings.html:16
+msgid "Check-in"
+msgstr "Vastaanotto"
+
+#: templates/admin/settings.html:35
+msgid "Update prices"
+msgstr "Päivitä hinnat"
+
+#: templates/admin/settings.html:37
+msgid "Applies the new settings to existing products after saving"
+msgstr ""
+
+#: templates/admin/settings.html:52 views/admin.py:622
+msgid "Notifications"
+msgstr "Ilmoitukset"
+
+#: templates/admin/settings.html:67
+msgid "Sending SMS messages will be handled by Servo"
+msgstr ""
+
+#: templates/admin/settings.html:98
+#, fuzzy
+msgid "Shipping Methods"
+msgstr "Kuljetuskulut"
+
+#: templates/admin/settings.html:105
+msgid "Add Method"
+msgstr ""
+
+#: templates/admin/checklist/form.html:10
+msgid "Tasks"
+msgstr "Tehtävät"
+
+#: templates/admin/fields/remove.html:6
+msgid "Are you sure you want to delete this field?"
+msgstr "Haluatko varmasti poistaa tämän kentän?"
+
+#: templates/admin/gsx/index.html:5
+msgid "New Account"
+msgstr "Luo uusi tili"
+
+#: templates/admin/gsx/remove.html:5
+msgid "Remove GSX account"
+msgstr "Poista GSX tili"
+
+#: templates/admin/notifications/index.html:6
+msgid "Reports"
+msgstr "Raportoi"
+
+#: templates/admin/notifications/index.html:15
+msgid "Daily"
+msgstr "Joka päivä"
+
+#: templates/admin/notifications/index.html:16
+msgid "Weekly"
+msgstr "Joka viikko"
+
+#: templates/admin/notifications/index.html:17
+msgid "Monthly"
+msgstr "Joka kuukausi"
+
+#: templates/admin/notifications/index.html:22
+msgid "Repairs aging beyond limits"
+msgstr ""
+
+#: templates/admin/notifications/index.html:28
+msgid "Products below stocking limit"
+msgstr "Tuotteita alle saldorajan"
+
+#: templates/admin/queues/form.html:11
+msgid "Defaults"
+msgstr "Oletus-statukset"
+
+#: templates/admin/queues/form.html:28
+msgid "Time limits"
+msgstr "Aikarajat"
+
+#: templates/admin/queues/form.html:46
+#, fuzzy
+msgid "Add Status"
+msgstr "Status"
+
+#: templates/admin/queues/remove.html:5
+#, python-format
+msgid "Delete queue \"%(title)s\"?"
+msgstr "Poistetaanko jono \"%(title)s\"?"
+
+#: templates/admin/queues/remove.html:9
+msgid "Orders in this queue will not be deleted."
+msgstr "Jonon tilauksia ei poisteta."
+
+#: templates/admin/sites/index.html:6 views/admin.py:648
+#, fuzzy
+msgid "New Site"
+msgstr "Uusi kenttä"
+
+#: templates/admin/sites/index.html:12
+msgid "Sites"
+msgstr ""
+
+#: templates/admin/sites/index.html:20
+#, fuzzy
+msgid "No site selected"
+msgstr "Merkintä tallennettu"
+
+#: templates/admin/statuses/remove.html:5
+#, python-format
+msgid "Delete status \"%(title)s\"?"
+msgstr "Poistetaanko status \"%(title)s\"?"
+
+#: templates/admin/tags/index.html:25
+#, fuzzy, python-format
+msgid "No %(objects)s found."
+msgstr "Ei yhtään tuotetta"
+
+#: templates/admin/users/delete_group.html:4
+msgid "Really delete this group?"
+msgstr "Poistetaanko tämä ryhmä?"
+
+#: templates/admin/users/delete_group.html:5
+msgid "Group member accounts will not be deleted."
+msgstr "Tämä ei poista ryhmän jäseniä."
+
+#: templates/admin/users/groups.html:13
+msgid "Members"
+msgstr "Jäsenet"
+
+#: templates/admin/users/groups.html:31
+msgid "No groups defined"
+msgstr "Ei yhtään ryhmää"
+
+#: templates/admin/users/index.html:13 views/admin.py:675
+#, fuzzy
+msgid "Upload Users"
+msgstr "Lähetä laitetiedot"
+
+#: templates/admin/users/remove.html:5
+#, python-format
+msgid "Delete user %(user)s?"
+msgstr "Poistetaanko käyttäjä %(user)s?"
+
+#: templates/admin/users/tabs.html:5
+msgid "Users"
+msgstr "Käyttäjät"
+
+#: templates/checkin/choose_model.html:6
+msgid "Choose your device"
+msgstr "Valitse laite"
+
+#: templates/checkin/choose_model.html:7
+msgid "Please choose your device model"
+msgstr "Ole hyvä ja valitse laitteesi malli"
+
+#: templates/checkin/choose_model.html:14
+msgid "MacBook Air"
+msgstr ""
+
+#: templates/checkin/choose_model.html:20
+msgid "MacBook"
+msgstr ""
+
+#: templates/checkin/choose_model.html:26
+msgid "MacBook Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:32
+msgid "iMac"
+msgstr ""
+
+#: templates/checkin/choose_model.html:40
+#, fuzzy
+msgid "iPhone"
+msgstr "Puhelin"
+
+#: templates/checkin/choose_model.html:46
+msgid "iPad"
+msgstr ""
+
+#: templates/checkin/choose_model.html:52
+msgid "Apple TV"
+msgstr ""
+
+#: templates/checkin/choose_model.html:60
+msgid "Mac Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:66
+#, fuzzy
+msgid "Display"
+msgstr "Toimita"
+
+#: templates/checkin/choose_model.html:72
+msgid "Mac mini"
+msgstr ""
+
+#: templates/checkin/choose_model.html:78
+#, fuzzy
+msgid "Power Mac"
+msgstr "Virtalähde"
+
+#: templates/checkin/choose_model.html:86
+msgid "iPod Touch"
+msgstr ""
+
+#: templates/checkin/choose_model.html:92
+msgid "iPod nano"
+msgstr ""
+
+#: templates/checkin/choose_model.html:98
+msgid "iPod Shuffle"
+msgstr ""
+
+#: templates/checkin/choose_model.html:104
+msgid "iPod Classic"
+msgstr ""
+
+#: templates/checkin/choose_model.html:112
+msgid "Keyboard or Mouse"
+msgstr "Näppäimistö tai hiiri"
+
+#: templates/checkin/choose_model.html:118 views/checkin.py:283
+msgid "Power Adapter"
+msgstr "Virtalähde"
+
+#: templates/checkin/choose_model.html:124
+msgid "AirPort and Wireless"
+msgstr "Langaton laite"
+
+#: templates/checkin/choose_model.html:130
+msgid "Other Device"
+msgstr "Muu laite"
+
+#: templates/checkin/customer.html:7
+msgid "Your contact details"
+msgstr "Sinun yhteystietosi"
+
+#: templates/checkin/customer.html:8
+msgid "Please make sure your contact details are filled out correctly."
+msgstr "Ole hyvä ja varmista että yhteystietosi ovat täytetty oikein."
+
+#: templates/checkin/device.html:8
+msgid "Please provide additional details about your device here."
+msgstr "Ole hyvä ja tarkenna laitteesi tietoja tässä"
+
+#: templates/checkin/device.html:8
+msgid ""
+"Providing a username and password helps us diagnose the problem faster, "
+"especially with software-related issues."
+msgstr ""
+"Käyttäjätunnuksen ja salasanan antaminen auttaa meitä selvittämään ongelman "
+"nopeammin, erityisesti ohjelmistoon liittyvissä ongelmissa."
+
+#: templates/checkin/device.html:8
+msgid "For faster service time, please upload your purchase receipt."
+msgstr ""
+"Ole hyvä ja liitä laitteesi ostokuitti jotta voisimme palvella sinua "
+"mahdollisimman nopeasti."
+
+#: templates/checkin/device.html:20
+msgid "Purchased"
+msgstr "Hankittu"
+
+#: templates/checkin/device.html:24 templates/devices/get_info.html:17
+#: templates/devices/search_gsx_warranty.html:17
+#: templates/devices/summary.html:16 templates/orders/devices.html:23
+#: templates/search/results/gsx_warranty.html:17
+msgid "Configration"
+msgstr "Kokoonpano"
+
+#: templates/checkin/device.html:28 templates/devices/get_info.html:23
+#: templates/devices/summary.html:34
+#: templates/search/results/gsx_warranty.html:22
+msgid "Activation Profile"
+msgstr "Aktivointiprofiili"
+
+#: templates/checkin/device.html:30 templates/devices/get_info.html:25
+#: templates/devices/search_gsx_warranty.html:26
+#: templates/devices/summary.html:36 templates/orders/devices.html:33
+#: templates/search/results/gsx_warranty.html:24
+msgid "Unlocked"
+msgstr "Lukitsematon"
+
+#: templates/checkin/device.html:34 templates/customers/form.html:9
+#: templates/devices/form.html:9 templates/devices/get_info.html:37
+#: templates/devices/search_gsx_warranty.html:36
+#: templates/orders/devices.html:45 templates/orders/gsx_repair_form.html:40
+#: templates/products/form.html:14
+msgid "Details"
+msgstr "Lisätiedot"
+
+#: templates/checkin/device.html:38
+#: templates/devices/search_gsx_warranty.html:40
+#, fuzzy
+msgid "Onsite Coverage"
+msgstr "Takuustatus"
+
+#: templates/checkin/device.html:42
+#: templates/devices/search_gsx_warranty.html:44
+#, fuzzy
+msgid "Contract Coverage"
+msgstr "Takuustatus"
+
+#: templates/checkin/device.html:57 templates/checkin/issue.html:20
+#: templates/checkin/sn.html:16
+msgid "Continue"
+msgstr "Jatka"
+
+#: templates/checkin/error.html:4
+msgid "An error occurred"
+msgstr "Tapahtui virhe"
+
+#: templates/checkin/error.html:12
+msgid "Try again"
+msgstr "Yritä uudestaan"
+
+#: templates/checkin/issue.html:8
+msgid "Please describe the problem in as much detail as possible."
+msgstr "Ole ja hyvä ja kuvaile ongelmaa mahdollisimman tarkasti."
+
+#: templates/checkin/sn.html:7
+msgid "Please enter your serial number"
+msgstr "Ole hyvä ja syötä laitteesi sarjanumero"
+
+#: templates/checkin/sn.html:8
+msgid ""
+"You can find instructions for locating the serial number by <a href=\"http://"
+"support.apple.com/kb/ht1349\" target=\"_blank\">clicking here</a>."
+msgstr ""
+"Ohjeet sarjanumeron paikantamiseen löydät <a href=\"http://support.apple.com/"
+"kb/HT1349?viewlocale=fi_FI\" target=\"_blank\">täältä</a>."
+
+#: templates/checkin/sn.html:8
+msgid ""
+"Clicking \"Choose\" will let you pick the type of your device if you don't "
+"know the serial number."
+msgstr ""
+
+#: templates/checkin/sn.html:8
+msgid "If you don't provide a serial number, the service time may be longer."
+msgstr "Huoltoaika saattaa venyä ilman laitteen sarjanumeroa."
+
+#: templates/checkin/sn.html:9
+msgid ""
+"When checking in an accessory, please provide the serial number of the "
+"device that the accessory is used with."
+msgstr ""
+"Jos kysessä on oheislaite, ole hyvä ja anna tietokoneen tai iOS-laitteen "
+"sarjanumero jonka kanssa oheislaitetta käytetään."
+
+#: templates/checkin/sn.html:9
+msgid ""
+"For example the serial number of your iPhone if the headset is broken, or "
+"the serial number of your Mac if you have a problem with the mouse."
+msgstr ""
+"Esimerkiksi iPhonen sarjanumero jos kuulokkeet ovat rikki tai Macin "
+"sarjanumero jos vika on hiiressä."
+
+#: templates/checkin/sn.html:15 templates/orders/customer.html:12
+#: templates/orders/statuses.html:3
+msgid "Choose"
+msgstr "Valitse"
+
+#: templates/checkin/status-show.html:33
+msgid "Refresh"
+msgstr ""
+
+#: templates/checkin/status-show.html:34
+msgid "Return"
+msgstr "Valmis"
+
+#: templates/checkin/status.html:7
+msgid "Please enter your Service Order number"
+msgstr "Ole hyvä ja anna huoltotilauksen numero"
+
+#: templates/checkin/status.html:9
+msgid ""
+"The Service Order number is an 8-digit code printed on your work confirmation"
+msgstr ""
+"Huoltotilauksen numero 8-merkkinen numero jonka löydät "
+"huoltotilausvahvistuksesta"
+
+#: templates/checkin/terms.html:6
+#, python-format
+msgid "%(company)s terms of service"
+msgstr "%(company)s huoltoehdot"
+
+#: templates/checkin/thanks.html:7 views/checkin.py:228
+msgid "Done!"
+msgstr "Valmis"
+
+#: templates/checkin/thanks.html:8
+msgid "Your service order has been submitted."
+msgstr "Huoltotilauksenne on lähetetty käsiteltäväksi."
+
+#: templates/checkin/thanks.html:8
+msgid "Click <strong>Print</strong> to print the confirmation."
+msgstr ""
+"Tulosta tilausvahvistus napsauttamalla <strong>Tulosta</strong>-nappia."
+
+#: templates/checkin/thanks.html:8
+#, python-format
+msgid "You will be redirected to the start page in %(seconds)s seconds."
+msgstr "Sinut uudelleenohjataan aloitus-sivulle %(seconds)s sekunnin kuluttua."
+
+#: templates/checkin/troubleshoot.html:12
+msgid "Yes"
+msgstr "Kyllä"
+
+#: templates/checkin/troubleshoot.html:13
+msgid "No"
+msgstr "Ei"
+
+#: templates/checkin/welcome.html:7
+#, python-format
+msgid "Welcome to %(location)s service check-in."
+msgstr "Tervetuloa %(location)s:n huoltojen sisäänkirjausjärjestelmään."
+
+#: templates/checkin/welcome.html:8
+msgid ""
+"From here you can <strong>create</strong> a service order or check the "
+"<strong>status</strong> of an existing order."
+msgstr ""
+"Täältä voit joko <strong>luoda</strong> uuden huoltotilauksen tai tarkistaa "
+"nykyisen huoltotilauksen <strong>tilaa</strong>."
+
+#: templates/checkin/welcome.html:8
+msgid "Please choose an option below."
+msgstr "Ole hyvä ja valitse vaihtoehto."
+
+#: templates/checkin/welcome.html:11
+msgid "Check Status"
+msgstr "Tarkista tila"
+
+#: templates/customers/choose-list.html:6 templates/customers/find.html:91
+#: templates/customers/search.html:28
+#: templates/search/results/customers.html:24
+msgid "No customers found"
+msgstr "Ei yhtään hakutulosta"
+
+#: templates/customers/choose.html:5
+msgid "Search for customer"
+msgstr "Hae asiakasta"
+
+#: templates/customers/choose.html:11
+msgid "Name, email or phone number"
+msgstr "Nimi, sähköposti tai puhelinnumero"
+
+#: templates/customers/delete_group.html:5
+#, fuzzy
+msgid "Delete customer group?"
+msgstr "Poistetaanko käyttäjä %(user)s?"
+
+#: templates/customers/delete_group.html:9
+#, fuzzy
+msgid "This action will not delete the customers in this group."
+msgstr "Tämä ei poista tuoterhymän tuotteita."
+
+#: templates/customers/find.html:15 templates/customers/index.html:43
+#: templates/devices/find.html:7 templates/devices/index.html:19
+#: templates/generic/index.html:6 templates/notes/find.html:8
+#: templates/notes/list_notes.html:28
+msgid "Browse"
+msgstr "Selaa"
+
+#: templates/customers/find.html:73 templates/devices/find.html:53
+#: templates/devices/list.html:26 templates/products/get_info.html:55
+msgid "View"
+msgstr "Avaa"
+
+#: templates/customers/index.html:11 templates/customers/index.html.py:13
+#, fuzzy
+msgid "New Contact"
+msgstr "Luo kontakti"
+
+#: templates/customers/index.html:24 templates/devices/index.html:13
+msgid "Upload"
+msgstr "Lähetä"
+
+#: templates/customers/index.html:27 templates/customers/index.html.py:30
+#: views/admin.py:412
+msgid "Edit Group"
+msgstr "Muokkaa ryhmää"
+
+#: templates/customers/index.html:28 templates/customers/index.html.py:31
+#, fuzzy
+msgid "Delete Group"
+msgstr "Poista"
+
+#: templates/customers/index.html:57 templates/devices/specs.html:7
+#: templates/products/index.html:61 templates/products/view.html:11
+#: views/product.py:36 views/product.py:326
+msgid "All"
+msgstr "Kaikki"
+
+#: templates/customers/index.html:74
+msgid "No customer selected"
+msgstr "Valitse asiakas"
+
+#: templates/customers/merge.html:12 templates/customers/move.html:12
+#, fuzzy
+msgid "Customer name"
+msgstr "Asiakkaan nimi"
+
+#: templates/customers/merge.html:18 templates/customers/move.html:19
+#: templates/orders/close.html:15 templates/orders/toolbar.html:69
+#: templates/orders/toolbar.html.py:73 templates/products/choose.html:17
+#: templates/snippets/modal.html:11
+msgid "Close"
+msgstr "Sulje"
+
+#: templates/customers/move.html:5
+#, fuzzy
+msgid "Move customer"
+msgstr "Ei asiakasta"
+
+#: templates/customers/move.html:18
+msgid "Move to top"
+msgstr "Siirrä päätasolle"
+
+#: templates/customers/remove.html:9
+msgid ""
+"This will also delete all customer's contacts. Customer's orders will not be "
+"deleted"
+msgstr ""
+"Tämä poistaa myös kaikki asiakkaan kontaktit. Asiakkaan tilauksia ei "
+"poisteta."
+
+#: templates/customers/upload.html:5
+#, fuzzy
+msgid "Upload customer data"
+msgstr "Ei asiakasta"
+
+#: templates/customers/view.html:48
+#, fuzzy
+msgid "Contacts"
+msgstr "Luo kontakti"
+
+#: templates/customers/view.html:55
+msgid "Use in "
+msgstr "Vie tilaukselle "
+
+#: templates/customers/view.html:57 templates/devices/view.html:22
+#, fuzzy
+msgid "Use in current order"
+msgstr "Vie tilaukselle"
+
+#: templates/customers/view.html:61 templates/devices/find.html:50
+#: templates/devices/list.html:23
+#: templates/devices/search_gsx_warranty.html:58
+#: templates/devices/view.html:25
+#: templates/search/results/gsx_warranty.html:33
+msgid "Create Service Order"
+msgstr "Luo huoltotilaus"
+
+#: templates/customers/view.html:69 templates/customers/view.html.py:72
+#, fuzzy
+msgid "Move Customer"
+msgstr "Uusi asiakas"
+
+#: templates/customers/view.html:70 templates/customers/view.html.py:73
+#, fuzzy
+msgid "Merge Customer"
+msgstr "Uusi asiakas"
+
+#: templates/customers/view.html:108 templates/notes/list_notes.html:57
+msgid "No messages found"
+msgstr "Ei yhtään viestiä"
+
+#: templates/devices/accessories_edit.html:4
+msgid "Manage Accessories"
+msgstr "Lisälaitteet"
+
+#: templates/devices/accessories_edit.html:15
+#: templates/orders/customer.html:22 templates/orders/customer.html.py:24
+#: templates/orders/devices.html:120 templates/orders/followers.html:23
+#: templates/orders/products.html:77 templates/orders/remove_customer.html:15
+#: templates/orders/remove_device.html:15
+msgid "Remove"
+msgstr "Poista"
+
+#: templates/devices/accessories_edit.html:19
+msgid "No accessories added"
+msgstr "Ei yhtään lisälaitetta"
+
+#: templates/devices/choose-list.html:10 templates/devices/find.html:62
+#: templates/devices/search_gsx_repairs.html:24
+#: templates/search/results/gsx_notfound.html:3
+msgid "No search results"
+msgstr "Ei hakutuloksia"
+
+#: templates/devices/choose.html:5
+msgid "Search for device"
+msgstr "Hae laitetta"
+
+#: templates/devices/choose.html:11
+msgid "Serial number or IMEI code"
+msgstr "Sarjanumero tai IMEI-koodi"
+
+#: templates/devices/diagnostic_results.html:4
+#, fuzzy
+msgid "Result"
+msgstr "Testitulokset"
+
+#: templates/devices/diagnostic_results.html:6
+msgid "Tool"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:8
+msgid "Passed modules"
+msgstr ""
+
+#: templates/devices/diagnostics.html:4
+#, fuzzy
+msgid "Diagnostics"
+msgstr "Diagnoosi"
+
+#: templates/devices/diagnostics.html:11
+msgid "Fetching diagnostics..."
+msgstr "Haetaan testituloksia..."
+
+#: templates/devices/find.html:29 templates/devices/search_gsx_repairs.html:7
+#: templates/notes/view_note.html:23 templates/orders/list.html:12
+#: templates/products/view.html:132 templates/products/view.html.py:156
+#: templates/products/view.html:181 templates/purchases/list_pos.html:52
+msgid "Created"
+msgstr "Luotu"
+
+#: templates/devices/find.html:47 templates/devices/list.html:19
+#: templates/devices/view.html:20 templates/products/view.html:93
+msgid "Use in"
+msgstr "Vie tilaukselle"
+
+#: templates/devices/find.html:51 templates/devices/list.html:24
+msgid "Show Service Orders"
+msgstr "Näytä huoltotilaukset"
+
+#: templates/devices/form.html:27 templates/products/form.html:53
+msgid "Enter tag"
+msgstr "Syötä tägi"
+
+#: templates/devices/get_info.html:12
+#: templates/devices/search_gsx_warranty.html:13
+#: templates/devices/summary.html:11 templates/orders/devices.html:18
+#: templates/search/results/gsx_warranty.html:13
+msgid "Purchase Date"
+msgstr "Hankittu"
+
+#: templates/devices/index.html:8 templates/devices/index.html.py:11
+msgid "Show Parts"
+msgstr "Varaosat"
+
+#: templates/devices/index.html:36 templates/orders/print_confirmation.html:64
+msgid "Model"
+msgstr "Malli"
+
+#: templates/devices/index.html:40
+msgid "No Product Line selected"
+msgstr "Valitse laitemalli"
+
+#: templates/devices/index.html:58 templates/devices/list.html:35
+#: templates/search/results/devices.html:8
+msgid "No devices found"
+msgstr "Ei yhtään laitetta"
+
+#: templates/devices/index.html:68
+msgid "No device selected"
+msgstr "Valitse laite"
+
+#: templates/devices/parts.html:9 templates/shipments/edit_bulk_return.html:56
+#: templates/shipments/list_incoming.html:17
+msgid "Part"
+msgstr "Varaosa"
+
+#: templates/devices/parts.html:21
+#, python-format
+msgid "Substituted to %(code)s"
+msgstr ""
+
+#: templates/devices/parts.html:27 templates/products/choose-list.html:7
+#: templates/products/list_rows.html:54 templates/products/search.html:13
+#: templates/search/results/products.html:8
+msgid "No products found"
+msgstr "Ei yhtään tuotetta"
+
+#: templates/devices/remove.html:5
+#, python-format
+msgid "Delete device %(sn)s\"?"
+msgstr "Poistetaanko laite %(sn)s\"?"
+
+#: templates/devices/remove.html:9
+msgid "This will also remove it from all service orders."
+msgstr "Tämä poistaa laitteen kaikista huoltotilauksista."
+
+#: templates/devices/search_gsx.html:13 templates/devices/search_gsx.html:15
+#: templates/devices/search_gsx_error.html:10
+#: templates/orders/gsx_repair_form.html:31
+#: templates/repairs/get_details.html:22 templates/search/results/gsx.html:13
+#: templates/search/results/gsx.html:15
+#: templates/shipments/list_bulk_returns.html:12
+msgid "Parts"
+msgstr "Varaosat"
+
+#: templates/devices/search_gsx.html:18 templates/devices/search_gsx.html:20
+#: templates/devices/search_gsx_error.html:11
+#: templates/search/results/gsx.html:18 templates/search/results/gsx.html:20
+msgid "Repairs"
+msgstr "Huollot"
+
+#: templates/devices/search_gsx_parts.html:20
+#: templates/devices/search_gsx_parts.html:22
+msgid "Create Product"
+msgstr "Luo tuote"
+
+#: templates/devices/search_gsx_repairs.html:5
+msgid "Number"
+msgstr "Numero"
+
+#: templates/devices/search_gsx_warranty.html:31
+#: templates/purchases/view_po.html:19
+msgid "Sales Order"
+msgstr "Myyntitilaus"
+
+#: templates/devices/search_gsx_warranty.html:55
+#: templates/search/results/gsx_warranty.html:35
+msgid "Use in order"
+msgstr "Vie tilaukselle"
+
+#: templates/devices/search_gsx_warranty.html:60
+#: templates/search/results/gsx_warranty.html:39
+#: templates/search/results/gsx_warranty.html:41
+msgid "Download Manual"
+msgstr "Lataa ohjeet"
+
+#: templates/devices/specs.html:8
+msgid "Specs"
+msgstr "Mallit"
+
+#: templates/devices/specs.html:10
+msgid "Tagi"
+msgstr "Tagi"
+
+#: templates/devices/specs.html:17
+msgid "Luo malli"
+msgstr "Luo malli"
+
+#: templates/devices/upload_devices.html:5
+msgid "Upload devices"
+msgstr "Lähetä laitetiedot"
+
+#: templates/devices/view.html:10 templates/devices/view.html.py:12
+#, fuzzy
+msgid "GSX Repairs"
+msgstr "GSX korjaukset"
+
+#: templates/devices/view.html:33 templates/devices/view.html.py:35
+msgid "Service Manual"
+msgstr "Huolto-ohje"
+
+#: templates/devices/view.html:44 templates/devices/view.html.py:47
+#: templates/orders/devices.html:93
+msgid "Update Warranty Status"
+msgstr "Päivitä takuutiedot"
+
+#: templates/devices/view.html:45 templates/devices/view.html.py:48
+#, fuzzy
+msgid "Fetch Diagnostics"
+msgstr "Hae diagnostiikka"
+
+#: templates/devices/view.html:73
+msgid "Fetching repairs..."
+msgstr "Haetaan korjauksia..."
+
+#: templates/generic/index.html:22
+msgid "Create New"
+msgstr "Luo uusi"
+
+#: templates/invoices/index.html:7 templates/invoices/index.html.py:105
+#: templates/products/tabs.html:17 templates/products/view.html:46
+#: views/invoices.py:23
+msgid "Invoices"
+msgstr "Laskut"
+
+#: templates/invoices/index.html:8
+msgid "Sales Invoices"
+msgstr "Myyntilaskut"
+
+#: templates/invoices/index.html:9
+msgid "Apple Invoices"
+msgstr "Apple-laskut"
+
+#: templates/invoices/index.html:46
+msgid "Date Created"
+msgstr "Luotu"
+
+#: templates/invoices/index.html:49
+msgid "Date Paid"
+msgstr "Maksettu"
+
+#: templates/invoices/index.html:71
+msgid "Print receipt"
+msgstr "Tulosta kuitti"
+
+#: templates/invoices/index.html:78
+msgid "No invoices found"
+msgstr "Ei yhtään laskua"
+
+#: templates/notes/form.html:38
+msgid "Attachments"
+msgstr "Liitteet"
+
+#: templates/notes/form.html:39
+msgid "Labels"
+msgstr "Merkit"
+
+#: templates/notes/form.html:40
+#, fuzzy
+msgid "Escalation"
+msgstr "Uusi sijainti"
+
+#: templates/notes/form.html:110
+#, fuzzy
+msgid "Edit Note"
+msgstr "Lisää merkintä"
+
+#: templates/notes/list_notes.html:11
+#, fuzzy
+msgid "New Message"
+msgstr "Tekstiviestit"
+
+#: templates/notes/list_notes.html:12
+#, fuzzy
+msgid "New Escalation"
+msgstr "Uusi sijainti"
+
+#: templates/notes/list_notes.html:37
+msgid "Inbox"
+msgstr ""
+
+#: templates/notes/list_notes.html:38
+msgid "Flagged"
+msgstr "Merkityt"
+
+#: templates/notes/list_notes.html:39
+msgid "Sent"
+msgstr "Lähetetyt"
+
+#: templates/notes/list_notes.html:40
+#, fuzzy
+msgid "Escalations"
+msgstr "Uusi sijainti"
+
+#: templates/notes/list_notes.html:65
+msgid "No message selected"
+msgstr "Valitse viesti"
+
+#: templates/notes/messages.html:5 templates/orders/notes.html:48
+#, fuzzy
+msgid "Message Log"
+msgstr "Viesti"
+
+#: templates/notes/messages.html:18
+#, fuzzy
+msgid "No messages to display"
+msgstr "Ei yhtään viestiä"
+
+#: templates/notes/remove.html:5
+msgid "Delete this note?"
+msgstr "Poistetaanko merkintä?"
+
+#: templates/notes/remove.html:9
+msgid "This will also delete any replies to this note."
+msgstr "Tämä poistaa myös merkinnän vastaukset."
+
+#: templates/notes/search-results.html:21
+#, fuzzy
+msgid "No notes found"
+msgstr "Ei yhtään laskua"
+
+#: templates/notes/search.html:7
+msgid "Create Note"
+msgstr "Luo merkintä"
+
+#: templates/notes/view_note.html:24
+msgid "Subject"
+msgstr "Otsikko"
+
+#: templates/notes/view_note.html:35 templates/orders/notes.html:21
+msgid "Reply"
+msgstr "Vastaa"
+
+#: templates/orders/close.html:5
+msgid "Close order"
+msgstr "Sulje tilaus"
+
+#: templates/orders/close.html:9
+msgid "This order will no longer be editable."
+msgstr "Tilausta ei voi enää muokata."
+
+#: templates/orders/close_repair.html:5
+msgid "Complete the GSX repair?"
+msgstr "Suljetaanko GSX korjaus?"
+
+#: templates/orders/close_repair.html:9
+msgid "The GSX repair will no longer be editable."
+msgstr "GSX korjausta ei tämän jälkeen voi enää muokata."
+
+#: templates/orders/close_repair.html:15
+msgid "Complete"
+msgstr "Sulje"
+
+#: templates/orders/customer.html:14 templates/orders/customer.html.py:16
+msgid "Add contact"
+msgstr "Lisää kontakti"
+
+#: templates/orders/delete_order.html:5
+#, python-format
+msgid "Really delete order %(id)s? "
+msgstr "Poistetaanko tilaus %(id)s? "
+
+#: templates/orders/delete_order.html:9
+msgid ""
+"This will also delete all the order's notes. This action cannot be undone."
+msgstr "Tämä poistaa myös tilauksen merkinnät. Toimintoa ei voi peruuttaa."
+
+#: templates/orders/devices.html:61 templates/orders/devices.html.py:63
+msgid "View Parts"
+msgstr "Varaosat"
+
+#: templates/orders/devices.html:63
+msgid "Assign order to a queue first"
+msgstr ""
+
+#: templates/orders/devices.html:65
+msgid "View History"
+msgstr "Historia"
+
+#: templates/orders/devices.html:67 templates/orders/devices.html.py:69
+msgid "View Accessories"
+msgstr "Lisälaitteet"
+
+#: templates/orders/devices.html:78 templates/orders/devices.html.py:80
+msgid "Create Carry-In Repair"
+msgstr ""
+
+#: templates/orders/devices.html:80
+#, fuzzy
+msgid "Device has open GSX repairs"
+msgstr "Sulje GSX korjaus automaattisesti"
+
+#: templates/orders/devices.html:83 templates/orders/devices.html.py:85
+#, fuzzy
+msgid "Create Onsite Repair"
+msgstr "Sulje korjaus"
+
+#: templates/orders/devices.html:88 templates/orders/devices.html.py:90
+#, fuzzy
+msgid "Create Whole-Unit Exchange"
+msgstr "Luotu hetki sitten"
+
+#: templates/orders/devices.html:94
+msgid "Get Diagnostics"
+msgstr "Hae diagnostiikka"
+
+#: templates/orders/devices.html:97 templates/orders/devices.html.py:99
+#: templates/orders/toolbar.html:91
+#, fuzzy
+msgid "Show Barcode"
+msgstr "Varaosat"
+
+#: templates/orders/devices.html:102
+msgid "Show Service Manual"
+msgstr "Huolto-ohje"
+
+#: templates/orders/devices.html:105
+msgid "Show Exploded View"
+msgstr "Avattu näkymä"
+
+#: templates/orders/devices.html:112 templates/orders/devices.html.py:114
+msgid "Report"
+msgstr "Raportoi"
+
+#: templates/orders/devices.html:131
+msgid "Order doesn't contain any devices"
+msgstr "Tilaus ei sisällä laitteita"
+
+#: templates/orders/devices.html:136 templates/orders/toolbar.html:14
+msgid "Add Device"
+msgstr "Lisää laite"
+
+#: templates/orders/dispatch.html:40
+msgid "Payment"
+msgstr "Maksutapa"
+
+#: templates/orders/dispatch.html:58 templates/products/index.html:100
+#: templates/products/search.html:18 templates/products/tabs.html:8
+#: templates/products/view.html:25 templates/purchases/edit_po.html:21
+#: templates/purchases/list_pos.html:58 views/product.py:31
+msgid "Products"
+msgstr "Tuotteet"
+
+#: templates/orders/dispatch.html:64
+#: templates/orders/print_confirmation.html:108
+#: templates/orders/print_dispatch.html:16
+msgid "Qty"
+msgstr "Kpl"
+
+#: templates/orders/dispatch.html:65
+#: templates/orders/print_confirmation.html:110
+#: templates/orders/products.html:13 templates/products/view.html:131
+#: templates/products/view.html.py:157 templates/products/view.html:182
+msgid "Price"
+msgstr "Hinta"
+
+#: templates/orders/dispatch.html:66
+msgid "Price w/ tax"
+msgstr "Myyntihinta"
+
+#: templates/orders/dispatch.html:97 templates/orders/dispatch.html.py:106
+#: templates/orders/toolbar.html:64 templates/orders/toolbar.html.py:66
+#: templates/purchases/edit_po.html:22
+msgid "Dispatch"
+msgstr "Toimita"
+
+#: templates/orders/edit.html:23
+#, fuzzy
+msgid "Checkin Location"
+msgstr "Vastaanotto"
+
+#: templates/orders/edit.html:43
+#, fuzzy
+msgid "Checkout Location"
+msgstr "Luovutus"
+
+#: templates/orders/edit.html:69
+msgid "Select Customer"
+msgstr "Valitse asiakas"
+
+#: templates/orders/edit.html:78
+msgid "Place"
+msgstr "Paikka"
+
+#: templates/orders/edit.html:79
+msgid "Enter place"
+msgstr "Syötä paikka"
+
+#: templates/orders/edit.html:86
+msgid "No place"
+msgstr "Ei paikkaa"
+
+#: templates/orders/edit.html:92
+msgid "Enter label"
+msgstr "Syötä merkki"
+
+#: templates/orders/edit.html:97
+msgid "No labels"
+msgstr "Ei yhtään merkkiä"
+
+#: templates/orders/followers.html:5
+msgid "Followers"
+msgstr "Seuraajat"
+
+#: templates/orders/followers.html:6
+msgid "Enter name"
+msgstr "Syötä nimi"
+
+#: templates/orders/followers.html:19 templates/orders/followers.html.py:21
+msgid "Make primary"
+msgstr "Tee pääkäsittelijä"
+
+#: templates/orders/followers.html:27
+msgid "No followers"
+msgstr "Ei seuraajia"
+
+#: templates/orders/gsx_repair_form.html:32
+#, fuzzy
+msgid "Checking warranty"
+msgstr "Vastaanotto"
+
+#: templates/orders/gsx_repair_form.html:39
+msgid "Repair"
+msgstr "Korjaus"
+
+#: templates/orders/gsx_repair_form.html:42
+#, fuzzy
+msgid "Components"
+msgstr "Varaosaryhmä"
+
+#: templates/orders/gsx_repair_form.html:73
+#: templates/purchases/edit_po.html:85
+#: templates/shipments/edit_bulk_return.html:94
+msgid "Confirm"
+msgstr "Vahvista"
+
+#: templates/orders/list.html:22
+msgid "No customer"
+msgstr "Ei asiakasta"
+
+#: templates/orders/list.html:22
+#, fuzzy
+msgid "No description"
+msgstr "kuvaus"
+
+#: templates/orders/list.html:29
+msgid "Nobody"
+msgstr "Ei kukaan"
+
+#: templates/orders/list.html:31
+msgid "No status"
+msgstr "Ei statusta"
+
+#: templates/orders/list.html:36 templates/orders/search.html:6
+#: templates/search/results/orders.html:8
+msgid "No orders found"
+msgstr "Ei yhtään tilausta"
+
+#: templates/orders/list_products.html:12
+msgid "Click to select EEE code"
+msgstr ""
+
+#: templates/orders/list_products.html:16
+#, fuzzy
+msgid "No products added"
+msgstr "Tuote %s lisätty"
+
+#: templates/orders/notes.html:8
+msgid "Order doesn't contain any notes."
+msgstr "Tilaus ei sisällä merkintöjä"
+
+#: templates/orders/notes.html:40
+msgid "Mark"
+msgstr "Merkitse"
+
+#: templates/orders/notes.html:47 templates/orders/repair.html:10
+msgid "Copy"
+msgstr "Kopioi"
+
+#: templates/orders/notes.html:65
+msgid "Note has been sent"
+msgstr ""
+
+#: templates/orders/notes.html:83 templates/orders/toolbar.html:6
+#: templates/orders/toolbar.html.py:8
+msgid "Add Note"
+msgstr "Lisää merkintä"
+
+#: templates/orders/parts.html:15
+#, fuzzy
+msgid "Parts and Services"
+msgstr "Tuotteet ja palvelut"
+
+#: templates/orders/parts.html:28
+#, fuzzy
+msgid "Fetching parts..."
+msgstr "Haetaan korjauksia..."
+
+#: templates/orders/parts.html:35 templates/stats/sales.html:21
+#, fuzzy
+msgid "Service Parts"
+msgstr "Huoltotilaus #%s"
+
+#: templates/orders/print_confirmation.html:26
+msgid "Repair Number"
+msgstr "Huoltonumero"
+
+#: templates/orders/print_confirmation.html:37
+msgid "Work Confirmation"
+msgstr "Huoltotilaus"
+
+#: templates/orders/print_confirmation.html:44
+msgid "Customer Information"
+msgstr "Asiakastiedot"
+
+#: templates/orders/print_confirmation.html:45
+msgid "Product Information"
+msgstr "Tuotetiedot"
+
+#: templates/orders/print_confirmation.html:53
+#: templates/orders/print_receipt.html:11
+msgid "Daytime Phone"
+msgstr "Puhelin"
+
+#: templates/orders/print_confirmation.html:66
+msgid "Estimated Date of Purchase"
+msgstr "Arvioitu ostopäivämäärä"
+
+#: templates/orders/print_confirmation.html:69
+msgid "Activation Policy"
+msgstr "Aktivointiprofiili"
+
+#: templates/orders/print_confirmation.html:89
+msgid "Work Description"
+msgstr "Työkuvaus"
+
+#: templates/orders/print_confirmation.html:101
+#: templates/orders/print_dispatch.html:9
+msgid "Products and Services"
+msgstr "Tuotteet ja palvelut"
+
+#: templates/orders/print_confirmation.html:107
+#: templates/orders/print_dispatch.html:15
+#, python-format
+msgid "VAT %%"
+msgstr "ALV %%"
+
+#: templates/orders/print_confirmation.html:109
+#, python-format
+msgid "0%% VAT"
+msgstr "0%% ALV"
+
+#: templates/orders/print_confirmation.html:119
+#: templates/orders/print_dispatch.html:24
+#: templates/orders/print_receipt.html:23
+msgid "SN"
+msgstr "SN"
+
+#: templates/orders/print_confirmation.html:151
+msgid "Customer Pickup"
+msgstr "Vastaanotto"
+
+#: templates/orders/print_confirmation.html:160
+#, fuzzy
+msgid "Customer Name"
+msgstr "Asiakkaan nimi"
+
+#: templates/orders/print_confirmation.html:161
+#: templates/orders/print_confirmation.html:178
+msgid "Signature"
+msgstr "Allekirjoitus"
+
+#: templates/orders/print_confirmation.html:168
+msgid "Received By"
+msgstr "Vastaanotettu"
+
+#: templates/orders/print_dispatch.html:5
+#, python-format
+msgid "Dispatch #%(code)s"
+msgstr "Toimitus #%(code)s"
+
+#: templates/orders/print_dispatch.html:25
+msgid "mon."
+msgstr "kk"
+
+#: templates/orders/print_receipt.html:6
+#, python-format
+msgid "Receipt %(number)s"
+msgstr "Kuitti %(number)s"
+
+#: templates/orders/print_receipt.html:40
+msgid "Date of invoice"
+msgstr "Kuitin päivämäärä"
+
+#: templates/orders/print_receipt.html:41
+msgid "Payment method"
+msgstr "Maksutapa"
+
+#: templates/orders/print_receipt.html:42
+msgid "Sales Person"
+msgstr "Teitä palveli"
+
+#: templates/orders/products.html:11 templates/purchases/edit_po.html:34
+#: templates/purchases/view_po.html:33
+msgid "Amount"
+msgstr "Määrä"
+
+#: templates/orders/products.html:12
+msgid "Price Category"
+msgstr "Hintaluokka"
+
+#: templates/orders/products.html:43 templates/orders/products.html.py:45
+msgid "Add to Repair"
+msgstr "Lisää korjaukseen"
+
+#: templates/orders/products.html:72 templates/orders/products.html.py:74
+msgid "Create Device"
+msgstr "Luo laite"
+
+#: templates/orders/products.html:86
+msgid "Order doesn't contain any products."
+msgstr "Tilaus ei sisällä tuotteita"
+
+#: templates/orders/products.html:91 templates/orders/toolbar.html:15
+#: templates/purchases/edit_po.html:6
+msgid "Add Product"
+msgstr "Lisää tuote"
+
+#: templates/orders/products.html:96
+msgid "Order Total"
+msgstr "Tilaus yhteensä"
+
+#: templates/orders/remove_customer.html:5
+msgid "Remove this customer from the order?"
+msgstr "Poistetaanko asiakas tästä tilauksesta?"
+
+#: templates/orders/remove_customer.html:9
+msgid "This will not delete the customer from the database."
+msgstr "Tämä ei poista asiakasta tietokannasta."
+
+#: templates/orders/remove_device.html:5
+#, python-format
+msgid "Remove %(dev)s?"
+msgstr "Poistetaanko %(dev)s?"
+
+#: templates/orders/remove_device.html:9
+msgid "This will not delete the device from the database."
+msgstr "Tämä ei poista laitetta kokonaan järjestelmästä."
+
+#: templates/orders/remove_product.html:5
+msgid "Really remove product from order?"
+msgstr "Poistetaanko tuote tiluaksesta?"
+
+#: templates/orders/remove_product.html:9
+msgid "Product will not be deleted from the warehouse."
+msgstr "Tuotetta ei poisteta varastosta"
+
+#: templates/orders/repair.html:6 templates/orders/repair.html.py:8
+msgid "Complete Repair"
+msgstr "Sulje korjaus"
+
+#: templates/orders/repair.html:16
+msgid "Symptom"
+msgstr "Vikakuvaus"
+
+#: templates/orders/repair.html:18
+msgid "Diagnosis"
+msgstr "Diagnoosi"
+
+#: templates/orders/repair.html:20
+#, fuzzy
+msgid "Request Review"
+msgstr "request review"
+
+#: templates/orders/reserve_products.html:5
+#, python-format
+msgid "Reserve all products in order %(id)s?"
+msgstr "Varataanko kaikki tilauksen %(id)s? tuotteet?"
+
+#: templates/orders/reserve_products.html:11
+msgid "Reserve"
+msgstr "Varaa"
+
+#: templates/orders/tabs.html:8
+msgid "Events"
+msgstr "Tapahtumat"
+
+#: templates/orders/toolbar.html:16
+#, fuzzy
+msgid "Choose Customer"
+msgstr "Valitse asiakas"
+
+#: templates/orders/toolbar.html:25
+#, fuzzy
+msgid "Cost Estimate"
+msgstr "Tulosta kustannusarvio"
+
+#: templates/orders/toolbar.html:27
+#, fuzzy
+msgid "Receipt"
+msgstr "Kuitti #%d"
+
+#: templates/orders/toolbar.html:28
+msgctxt "noun"
+msgid "Dispatch"
+msgstr "Lähete"
+
+#: templates/orders/toolbar.html:42 templates/orders/toolbar.html.py:44
+msgid "Remove Queue"
+msgstr "Poista jono"
+
+#: templates/orders/toolbar.html:59
+msgid "Remove Status"
+msgstr "Poista status"
+
+#: templates/orders/toolbar.html:71
+msgid "Reopen"
+msgstr "Avaa"
+
+#: templates/orders/toolbar.html:82
+#, fuzzy
+msgid "Unfollow Order"
+msgstr "Voi seurata tilausta"
+
+#: templates/orders/toolbar.html:84
+#, fuzzy
+msgid "Follow Order"
+msgstr "Seuraajat"
+
+#: templates/orders/toolbar.html:87 templates/orders/toolbar.html.py:89
+msgid "Copy Order"
+msgstr "Kopioi tilaus"
+
+#: templates/orders/toolbar.html:94
+msgid "Priority"
+msgstr "Prioriteetti"
+
+#: templates/orders/toolbar.html:103 templates/orders/toolbar.html.py:106
+msgid "Reserve Products"
+msgstr "Varaa tuotteet"
+
+#: templates/orders/toolbar.html:104 templates/orders/toolbar.html.py:107
+msgid "Order Products"
+msgstr "Tilaa tuotteet"
+
+#: templates/orders/toolbar.html:111 templates/orders/toolbar.html.py:113
+msgid "Delete Order"
+msgstr "Poista tilaus"
+
+#: templates/products/category_form.html:5
+msgid "Edit Product Group"
+msgstr "Muokkaa tuoteryhmää"
+
+#: templates/products/choose.html:5
+msgid "Search for a product"
+msgstr "Hae tuotetta"
+
+#: templates/products/choose.html:11
+msgid "Code, title or category"
+msgstr "Koodi, nimike tai tuoteryhmä"
+
+#: templates/products/delete_category.html:5
+msgid "Delete product category?"
+msgstr "Poistetaanko tuoteryhmä?"
+
+#: templates/products/delete_category.html:9
+msgid "This action will not delete the products in this category."
+msgstr "Tämä ei poista tuoterhymän tuotteita."
+
+#: templates/products/form.html:42
+msgid "Add"
+msgstr "Lisää"
+
+#: templates/products/get_info.html:6
+msgid "Product Info"
+msgstr "Tuotetiedot"
+
+#: templates/products/get_info.html:26 templates/products/view.html:67
+msgid "Component Group"
+msgstr "Varaosaryhmä"
+
+#: templates/products/get_info.html:31
+msgid "EEE Code"
+msgstr "EEE-koodi"
+
+#: templates/products/get_info.html:40 templates/products/view.html:82
+msgid "Stocked"
+msgstr "Varastossa"
+
+#: templates/products/get_info.html:42 templates/products/receive_item.html:15
+#: templates/products/view.html:84 templates/purchases/list_pos.html:53
+#: templates/shipments/list_incoming.html:20
+msgid "Ordered"
+msgstr "Tilattu"
+
+#: templates/products/get_info.html:44 templates/products/view.html:86
+msgid "Reserved"
+msgstr "Varattu"
+
+#: templates/products/get_info.html:49
+msgid "Product not in inventory"
+msgstr "Tuotetta ei ole varastossa"
+
+#: templates/products/index.html:15 templates/products/index.html.py:18
+msgid "New Sales Order"
+msgstr "Uusi myyntitilaus"
+
+#: templates/products/index.html:16 templates/products/index.html.py:19
+msgid "New Purchase Order"
+msgstr "Uusi ostotilaus"
+
+#: templates/products/index.html:30 templates/products/index.html.py:33
+msgid "Edit Category"
+msgstr "Muokka tuoteryhmää"
+
+#: templates/products/index.html:31 templates/products/index.html.py:34
+msgid "Delete Category"
+msgstr "Poista tuoteryhmä"
+
+#: templates/products/index.html:37
+msgid "Download Products"
+msgstr "Lataa tuotteet"
+
+#: templates/products/index.html:38
+msgid "Upload Products"
+msgstr "Lähetä tuotteet"
+
+#: templates/products/index.html:39
+msgid "Upload Parts Database"
+msgstr "Lähetä GSX varaosat"
+
+#: templates/products/index_outgoing.html:10
+#: templates/products/index_outgoing.html:55
+msgid "Outgoing"
+msgstr "Lähtevät"
+
+#: templates/products/index_outgoing.html:11
+msgid "Shipped"
+msgstr "Palautettu"
+
+#: templates/products/index_outgoing.html:23
+#: templates/purchases/edit_po.html:31
+#: templates/shipments/list_returns.html:12
+msgid "Code"
+msgstr "Koodi"
+
+#: templates/products/index_outgoing.html:26
+#: templates/shipments/edit_bulk_return.html:58
+#: templates/shipments/list_returns.html:15
+msgid "Return Order"
+msgstr "Palautustilaus"
+
+#: templates/products/index_outgoing.html:27
+#: templates/shipments/list_returns.html:16
+msgid "Register For Return"
+msgstr "Lisätty palautuslistalle"
+
+#: templates/products/index_outgoing.html:44
+#: templates/shipments/list_returns.html:33
+msgid "No parts pending return"
+msgstr "Ei yhtään lähtevää varaosaa"
+
+#: templates/products/list.html:10
+msgid "In Stock"
+msgstr "Varastossa"
+
+#: templates/products/list_rows.html:31
+msgid "Use in Service Order"
+msgstr "Vie tilaukselle"
+
+#: templates/products/list_rows.html:35 templates/products/view.html:96
+msgid "Create Sales Order"
+msgstr "Luo myyntitilaus"
+
+#: templates/products/list_rows.html:36
+msgid "Create Purchase Order"
+msgstr "Luo ostotilaus"
+
+#: templates/products/list_rows.html:39
+msgid "Use in Purchase Order"
+msgstr "Vie ostotilaukselle"
+
+#: templates/products/receive_item.html:21
+msgid "Stocking Order"
+msgstr "Varastoon"
+
+#: templates/products/receive_item.html:49
+#: templates/shipments/list_incoming.html:52
+msgid "Receive"
+msgstr "Saavuta"
+
+#: templates/products/remove.html:5
+#, python-format
+msgid "Really delete product \"%(code)s\"?"
+msgstr "Poistetaanko tuote \"%(code)s\"?"
+
+#: templates/products/tabs.html:11
+msgid "Shipments"
+msgstr "Lähetykset"
+
+#: templates/products/tabs.html:14 templates/purchases/list_pos.html:131
+#: templates/purchases/view_po.html:55 views/purchases.py:28
+msgid "Purchase Orders"
+msgstr "Ostotilaukset"
+
+#: templates/products/upload_gsx_parts.html:5
+msgid "Upload GSX Parts"
+msgstr "Lähetä GSX varaosat"
+
+#: templates/products/view.html:44 templates/stats/index.html:12
+#: templates/stats/locations.html:9 templates/stats/queues.html:9
+#: templates/stats/sales.html:9 templates/stats/sales.html.py:13
+#: templates/stats/statuses.html:9
+msgid "Sales"
+msgstr "Myynti"
+
+#: templates/products/view.html:45 templates/stats/sales.html:17
+msgid "Purchases"
+msgstr "Ostot"
+
+#: templates/products/view.html:104 templates/products/view.html.py:106
+#: templates/products/view.html:111
+msgid "Update Price"
+msgstr "Päivitä hinnat"
+
+#: templates/products/view.html:133
+msgid "Dispatched"
+msgstr "Toimitettu"
+
+#: templates/products/view.html:147
+msgid "No Sales Orders"
+msgstr "Ei yhtään myyntitilausta"
+
+#: templates/products/view.html:171 templates/purchases/list_pos.html:106
+msgid "No Purchase Orders"
+msgstr "Ei yhtään ostotilausta"
+
+#: templates/products/view.html:195
+msgid "No invoices"
+msgstr "Ei yhtään laskua"
+
+#: templates/purchases/edit_po.html:61
+msgid "Order does not contain any products"
+msgstr "Tilaus ei sisällä tuotteita"
+
+#: templates/purchases/list_pos.html:7
+msgid "New"
+msgstr "Uusi"
+
+#: templates/purchases/list_pos.html:86 templates/purchases/list_pos.html:88
+msgid "Create GSX Stocking Order"
+msgstr "Luo GSX stock-tilaus"
+
+#: templates/purchases/order_stock.html:5
+msgid "Submit this stocking order?"
+msgstr "Lähetetäänkö ostotilaus?"
+
+#: templates/purchases/order_stock.html:9
+msgid "Products will be ordered from GSX."
+msgstr "Tuotteet tilataan GSX:stä"
+
+#: templates/purchases/view_po.html:15
+msgid "Created By"
+msgstr "Luoja"
+
+#: templates/purchases/view_po.html:17
+msgid "Created At"
+msgstr "Luotu"
+
+#: templates/purchases/view_po.html:35
+msgid "Received At"
+msgstr "Saavutettu"
+
+#: templates/repairs/add_part.html:5
+#, python-format
+msgid "Add part to repair %(repair)s?"
+msgstr "Lisätäänkö varaosa korjaukseen %(repair)s?"
+
+#: templates/repairs/delete_part.html:5
+msgid "Remove this part from the GSX repair?"
+msgstr "Poistetaanko varaosa GSX korjauksesta?"
+
+#: templates/repairs/delete_repair.html:5
+#, fuzzy
+msgid "Delete this GSX repair?"
+msgstr "Poistetaanko tämä GSX korjaus?"
+
+#: templates/repairs/delete_repair.html:9
+msgid "Only repairs that have not been submitted can be deleted."
+msgstr ""
+
+#: templates/repairs/get_details.html:12
+#: templates/search/results/gsx_repair_details.html:8
+msgid "CS Code"
+msgstr "CS-koodi"
+
+#: templates/repairs/get_details.html:14
+#: templates/search/results/gsx_repair_details.html:10
+msgid "Tracking Number"
+msgstr "Seurantakoodi"
+
+#: templates/repairs/get_details.html:18
+#: templates/search/results/gsx_repair_details.html:14
+msgid "Warranty Coverage"
+msgstr "Takuustatus"
+
+#: templates/repairs/part_menu.html:4 templates/repairs/part_menu.html.py:7
+msgid "Print Return Label"
+msgstr "Tulosta palautuslähete"
+
+#: templates/repairs/part_menu.html:7
+msgid "Part has no return order number"
+msgstr "Varaosalla ei ole palautustilausnumeroa"
+
+#: templates/repairs/part_menu.html:11 templates/repairs/part_menu.html:13
+msgid "Update Serial Numbers"
+msgstr "Päivitä sarjanumerot"
+
+#: templates/repairs/part_menu.html:17 templates/repairs/part_menu.html:27
+msgid "Return DOA"
+msgstr "Palauta DOA"
+
+#: templates/repairs/part_menu.html:18 templates/repairs/part_menu.html:28
+#: views/shipments.py:326
+msgid "Return Good Part"
+msgstr "Palauta Good Part"
+
+#: templates/repairs/part_menu.html:19 templates/repairs/part_menu.html:29
+#: views/shipments.py:330
+msgid "Convert to Stock"
+msgstr ""
+
+#: templates/repairs/part_menu.html:22 templates/repairs/part_menu.html:24
+#: templates/repairs/part_menu.html:31
+msgid "Remove from Repair"
+msgstr "Poista korjauksesta"
+
+#: templates/search/spotlight.html:11
+msgid "Results"
+msgstr "Testitulokset"
+
+#: templates/search/spotlight.html:25
+msgid "Products and Parts"
+msgstr "Tuotteet ja varaosat"
+
+#: templates/search/spotlight.html:28
+msgid "Articles"
+msgstr "Artikkelit"
+
+#: templates/search/spotlight.html:36
+msgid "Home"
+msgstr "Kotisivu"
+
+#: templates/shipments/add_to_return-results.html:6
+msgid "No parts found"
+msgstr "Ei yhtään varaosaa"
+
+#: templates/shipments/add_to_return.html:5
+msgid "Add part to return"
+msgstr "Vie palautuslistalle"
+
+#: templates/shipments/add_to_return.html:11
+msgid "Return order number"
+msgstr "Palautustilausnumero"
+
+#: templates/shipments/edit_bulk_return.html:6
+msgid "Add Part"
+msgstr "Lisää varaosa"
+
+#: templates/shipments/edit_bulk_return.html:9
+#: templates/shipments/edit_bulk_return.html:11
+msgid "Verify"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:28
+#: templates/shipments/view_bulk_return.html:6
+msgid "Carrier"
+msgstr "Toimittaja"
+
+#: templates/shipments/edit_bulk_return.html:34
+#: templates/shipments/view_bulk_return.html:8
+msgid "Tracking"
+msgstr "seurantakoodi"
+
+#: templates/shipments/edit_bulk_return.html:40
+#: templates/shipments/view_bulk_return.html:10
+msgid "Dimensions"
+msgstr "Mitat"
+
+#: templates/shipments/edit_bulk_return.html:46
+#: templates/shipments/view_bulk_return.html:12
+msgid "Weight"
+msgstr "Paino"
+
+#: templates/shipments/edit_bulk_return.html:59
+msgid "Overpack"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:85
+#: templates/shipments/view_bulk_return.html:28
+msgid "No parts registered for this shipment"
+msgstr "Lähetyksessä ei ole yhtään osaa"
+
+#: templates/shipments/index.html:26 templates/shipments/list_incoming.html:60
+msgid "Incoming"
+msgstr "Saapuvat"
+
+#: templates/shipments/index.html:29 templates/shipments/list_returns.html:43
+msgid "Parts Pending Return"
+msgstr "Lähtevät varaosat"
+
+#: templates/shipments/index.html:31
+#: templates/shipments/list_bulk_returns.html:36
+msgid "Browse Returns"
+msgstr "Selaa palautuksia"
+
+#: templates/shipments/list_bulk_returns.html:9
+msgid "ID"
+msgstr "Tunniste"
+
+#: templates/shipments/list_bulk_returns.html:11
+msgid "Tracking URL"
+msgstr "Seurantaosoite"
+
+#: templates/shipments/list_bulk_returns.html:13
+#, fuzzy
+msgid "Returned"
+msgstr "Lähtevät"
+
+#: templates/shipments/list_incoming.html:46
+msgid "No incoming products"
+msgstr "Ei saapuvia tuotteita"
+
+#: templates/shipments/submit_bulk_return.html:4
+msgid "Submit the bulk return?"
+msgstr "Lähetetäänkö palautuslista"
+
+#: templates/shipments/view_bulk_return.html:15
+msgid "Open Packing List"
+msgstr ""
+
+#: templates/snippets/error_modal.html:5
+msgid "An error occured..."
+msgstr "Tapahtui virhe..."
+
+#: templates/stats/index.html:8 templates/stats/locations.html:5
+#: templates/stats/queues.html:5 templates/stats/sales.html:5
+#: templates/stats/statuses.html:5
+msgid "Technicians"
+msgstr ""
+
+#: templates/stats/index.html:40
+msgid "Orders Assigned"
+msgstr "Tilauksia käsittelyssä"
+
+#: templates/stats/index.html:41
+msgid ""
+"Shows how many new orders have been assigned to each technician over the "
+"given time period."
+msgstr ""
+
+#: templates/stats/index.html:44 templates/stats/locations.html:13
+#: templates/stats/queues.html:13
+msgid "Orders Created"
+msgstr "Luotuja tilauksia"
+
+#: templates/stats/index.html:45
+msgid "This graph shows how many orders are checked in by each user."
+msgstr ""
+
+#: templates/stats/index.html:48 templates/stats/locations.html:29
+#: templates/stats/queues.html:25
+msgid "Work Distribution"
+msgstr "Työnjako"
+
+#: templates/stats/index.html:49
+msgid ""
+"Shows you how the total number of service orders is distributed across the "
+"technicians at this location."
+msgstr ""
+
+#: templates/stats/locations.html:14
+msgid "Shows you how many orders are created at each location."
+msgstr ""
+
+#: templates/stats/locations.html:17 templates/stats/queues.html:17
+msgid "Orders Closed"
+msgstr "Suljettuja tilauksia"
+
+#: templates/stats/locations.html:18
+msgid "Shows you how many orders have been closed at each location."
+msgstr ""
+
+#: templates/stats/locations.html:21 templates/stats/queues.html:21
+msgid "Average Turnaround"
+msgstr ""
+
+#: templates/stats/locations.html:22
+msgid "Shows how many hours it takes to complete an order at each location."
+msgstr ""
+
+#: templates/stats/locations.html:25
+msgid "Average Runrate"
+msgstr ""
+
+#: templates/stats/locations.html:26
+msgid "Shows you how many orders people are working on at each location."
+msgstr ""
+
+#: templates/stats/locations.html:30
+msgid ""
+"This shows you how your overall work load is distributed across your service "
+"locations."
+msgstr ""
+
+#: templates/stats/queues.html:14
+msgid ""
+"This is your total number of orders per queue in the specified time period"
+msgstr ""
+
+#: templates/stats/queues.html:18
+msgid "Shows you how many orders have been closed in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:22
+msgid "Shows how many hours it takes to complete an order in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:26
+msgid ""
+"This shows your total ratio of orders over the time period distributed over "
+"each queue."
+msgstr ""
+
+#: templates/stats/sales.html:14
+msgid "Shows you invoice totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:18
+msgid ""
+"Shows you Purchase Order totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:22
+msgid "Shows you how many parts have been ordered for each labour tier."
+msgstr ""
+
+#: templates/stats/statuses.html:13
+msgid "Orders per location"
+msgstr "Tilauksia/toimipiste"
+
+#: templates/stats/statuses.html:14
+msgid ""
+"Shows the number of orders with a particular status at the selected location "
+"that have been assigned to a technician over the specified time period."
+msgstr ""
+
+#: templates/stats/statuses.html:17
+msgid "Orders per user"
+msgstr "Tilauksia/käyttäjä"
+
+#: templates/stats/statuses.html:18
+msgid ""
+"Shows the number of orders with a particular status per each user at the "
+"given location that have been assigned to a technician over the specified "
+"time period."
+msgstr ""
+
+#: views/account.py:26
+#, fuzzy
+msgid "Profile Settings"
+msgstr "Asetukset"
+
+#: views/account.py:34 views/admin.py:235
+msgid "Settings saved"
+msgstr "Asetukset tallennettu"
+
+#: views/account.py:47
+#, fuzzy
+msgid "Error in user details"
+msgstr "Virhe käyttäjätiedoissa"
+
+#: views/account.py:73
+msgid "My Orders"
+msgstr "Omat tilaukset"
+
+#: views/account.py:81
+msgid "Sign In"
+msgstr "Kirjaudu sisään"
+
+#: views/account.py:102
+#, python-format
+msgid "%s logged in"
+msgstr "%s kirjautui sisään"
+
+#: views/account.py:109
+msgid "Incorrect username or password"
+msgstr "Väärä tunnus tai salasana"
+
+#: views/account.py:111
+msgid "Login failed"
+msgstr ""
+
+#: views/account.py:119
+msgid "You have logged out"
+msgstr "Kirjauduit ulos"
+
+#: views/account.py:223
+msgid "Users can only delete their own calendars!"
+msgstr "Käyttäjät voi poistaa vain omia kalentereita!"
+
+#: views/account.py:229
+msgid "Calendar deleted"
+msgstr "Kalenteri poistettu"
+
+#: views/account.py:232
+msgid "Really delete this calendar?"
+msgstr "Poistetaanko tämä kalenteri?"
+
+#: views/account.py:251
+msgid "Calendar saved"
+msgstr "Kalenteri tallennettu"
+
+#: views/account.py:274
+msgid "Calendar event created"
+msgstr "Tapahtuma luotu"
+
+#: views/account.py:284
+msgid "Event saved"
+msgstr "Tapahtuma tallennettu"
+
+#: views/account.py:287
+msgid "Edit Event"
+msgstr "Muokkaa tapahtumaa"
+
+#: views/account.py:298
+msgid "Calendar event updated"
+msgstr "Tapahtuma päivitetty"
+
+#: views/account.py:305
+msgid "Users can only delete their own events!"
+msgstr "Käyttäjät voi poistaa vain omia tapahtumia!"
+
+#: views/account.py:313
+msgid "Calendar event deleted"
+msgstr "Tapahtuma poistettu"
+
+#: views/account.py:316
+msgid "Really delete this event?"
+msgstr "Poistetaanko tämä tapahtuma?"
+
+#: views/account.py:340
+msgid "Your registration is now pending approval."
+msgstr "Rekisteröinti on vastaanotettu"
+
+#: views/account.py:354
+#, fuzzy
+msgid "All notifications cleared"
+msgstr "Lähetä ilmoitukset sähköpostilla"
+
+#: views/account.py:365 views/order.py:855
+msgid "Search query is too short"
+msgstr ""
+
+#: views/account.py:381
+msgid "Search results"
+msgstr "Hakutulokset"
+
+#: views/admin.py:65 views/admin.py:270 views/customer.py:140
+#: views/device.py:152
+#, python-format
+msgid "%s saved"
+msgstr "%s tallennettu"
+
+#: views/admin.py:71
+msgid "GSX account for this sold-to and environment already exists"
+msgstr ""
+
+#: views/admin.py:83
+msgid "GSX account deleted"
+msgstr "GSX-tili poistettu"
+
+#: views/admin.py:126
+msgid "Checklist saved"
+msgstr "Muistilista tallennettu"
+
+#: views/admin.py:138
+msgid "Checklist deleted"
+msgstr "Muistilista poistettu"
+
+#: views/admin.py:142
+msgid "Really delete this checklist?"
+msgstr "Poistetaanko muistilista? "
+
+#: views/admin.py:143
+msgid "This will also delete all checklist values."
+msgstr "Tämä poistaa myös muistilistan vastaukset."
+
+#: views/admin.py:179
+#, python-format
+msgid "Tag %s saved"
+msgstr "Tagi %s tallennettu"
+
+#: views/admin.py:194
+msgid "Tag deleted"
+msgstr "Tagi poistettu"
+
+#: views/admin.py:197
+msgid "Really delete this tag?"
+msgstr "Poistetaanko tämä tagi?"
+
+#: views/admin.py:215
+msgid "Check your settings"
+msgstr "Tarkista asetukset"
+
+#: views/admin.py:283 views/admin.py:542 views/customer.py:154
+#, python-format
+msgid "%s deleted"
+msgstr "%s poistettu"
+
+#: views/admin.py:322
+msgid "Field saved"
+msgstr "Kenttä tallennettu"
+
+#: views/admin.py:334
+msgid "Field deleted"
+msgstr "Kenttä poistettu"
+
+#: views/admin.py:337
+msgid "Really delete this field?"
+msgstr "Poistetaanko tämä kenttä?"
+
+#: views/admin.py:367
+#, fuzzy, python-format
+msgid "Template %s saved"
+msgstr "Pohja tallennettu"
+
+#: views/admin.py:383
+#, python-format
+msgid "Template %s deleted"
+msgstr "Pohja %s poistettu"
+
+#: views/admin.py:386
+msgid "Really delete this template?"
+msgstr "Poistetaanko pohja?"
+
+#: views/admin.py:427
+msgid "Group saved"
+msgstr "Ryhmä tallennettu"
+
+#: views/admin.py:439
+msgid "Group deleted"
+msgstr "Ryhmä poistettu"
+
+#: views/admin.py:454
+msgid "User deleted"
+msgstr "Käyttäjä poistettu"
+
+#: views/admin.py:483
+#, python-format
+msgid "User %s saved"
+msgstr "Käyttäjä %s tallennettu"
+
+#: views/admin.py:486
+msgid "Error in user profile data"
+msgstr "Virhe käyttäjätiedoissa"
+
+#: views/admin.py:492
+#, fuzzy, python-format
+msgid "%d users"
+msgstr "%d käyttäjää"
+
+#: views/admin.py:527
+#, python-format
+msgid "Location %s saved"
+msgstr "Sijainti %s tallennettu"
+
+#: views/admin.py:548
+msgid "Really delete this location?"
+msgstr "Poistetaanko sijainti?"
+
+#: views/admin.py:549
+msgid "This will not delete the orders at this location"
+msgstr "Tämä ei poista toimipisteen tilauksia."
+
+#: views/admin.py:560
+msgid "Create, edit and delete service queues"
+msgstr ""
+
+#: views/admin.py:587
+msgid "Failed to save queue"
+msgstr ""
+
+#: views/admin.py:594
+#, python-format
+msgid "%s queue saved"
+msgstr "Jono %s tallennettu"
+
+#: views/admin.py:611
+msgid "Queue deleted"
+msgstr "Jono tallennettu"
+
+#: views/admin.py:613
+msgid "Cannot delete queue"
+msgstr ""
+
+#: views/admin.py:633 views/admin.py:644
+msgid "Access denied"
+msgstr "Pääsy evätty"
+
+#: views/admin.py:637
+#, fuzzy
+msgid "Manage Sites"
+msgstr "Lisälaitteet"
+
+#: views/admin.py:663
+#, fuzzy
+msgid "Site saved"
+msgstr "Merkintä tallennettu"
+
+#: views/admin.py:682
+#, fuzzy, python-format
+msgid "%d users imported"
+msgstr "%d laitetta tuotu"
+
+#: views/checkin.py:109
+msgid "Welcome"
+msgstr "Tervetuloa"
+
+#: views/checkin.py:120
+msgid "1/3: Enter serial number, IMEI code or choose a device"
+msgstr "1/3: Syötä sarjanumero, IMEI koodi tai valitse laite"
+
+#: views/checkin.py:123
+msgid "Please enable cookies in your browser"
+msgstr ""
+
+#: views/checkin.py:134 views/checkin.py:161
+msgid "Your session has expired, please try again."
+msgstr "Istuntosi vanhentui. Ole hyvä ja yritä uudelleen."
+
+#: views/checkin.py:140
+msgid "The serial number you entered is not valid."
+msgstr "Antamasi sarjanumero on virheellinen."
+
+#: views/checkin.py:142
+msgid ""
+"Couldn't check warranty status, please choose device type manually or try "
+"again later."
+msgstr ""
+"Takuutarkistus epäonnistui. Ole hyvä ja valitse laite käsin tai kokeile "
+"myöhemmin uudestaan."
+
+#: views/checkin.py:155
+msgid "1/3: Device details"
+msgstr "1/3: Laitteen tiedot"
+
+#: views/checkin.py:192
+msgid "2/3: Problem description"
+msgstr "2/3: Ongelmakuvaus"
+
+#: views/checkin.py:206
+msgid "3/3: Your contact details"
+msgstr "3/3: Yhteystietosi"
+
+#: views/checkin.py:234
+msgid "Order does not exist"
+msgstr "Tilausta ei löytynyt"
+
+#: views/checkin.py:244
+msgid "2/3: Troubleshooting"
+msgstr "2/3: Vianhaku"
+
+#: views/checkin.py:281
+msgid "1/3: Choose your device"
+msgstr "1/3: Valitse laitteesi"
+
+#: views/checkin.py:284
+msgid "Apple Keyboard"
+msgstr "Applen näppäimistö"
+
+#: views/checkin.py:285
+msgid "Apple Wireless Device"
+msgstr "Langaton laite"
+
+#: views/checkin.py:289
+#, python-format
+msgid "1/3: %s"
+msgstr ""
+
+#: views/checkin.py:320
+msgid "Repair Status"
+msgstr "Huollon tila"
+
+#: views/checkin.py:332
+msgid "Waiting to be processed"
+msgstr "Odottaa käsitelyä"
+
+#: views/checkin.py:334
+#, python-format
+msgid "Order %s not found"
+msgstr "Tilausta %s ei löytynyt"
+
+#: views/checkin.py:359
+#, fuzzy
+msgid "Your session has expired"
+msgstr "Istuntosi vanhentui. Ole hyvä ja yritä uudelleen."
+
+#: views/customer.py:89
+msgid "Customer added"
+msgstr "Asiakas lisätty"
+
+#: views/customer.py:105
+msgid "Customer not found"
+msgstr "Asiakasta ei löytynyt"
+
+#: views/customer.py:206
+msgid "Customer saved"
+msgstr "Asiakas tallennettu"
+
+#: views/customer.py:232
+msgid "Customer deleted"
+msgstr "Asiakas poistettu"
+
+#: views/customer.py:250
+#, python-format
+msgid "Merge %s with"
+msgstr ""
+
+#: views/customer.py:265
+msgid "Customer records merged succesfully"
+msgstr ""
+
+#: views/customer.py:281
+#, python-format
+msgid "Customer %s moved to top level"
+msgstr "Asiakas %s siirretty päätasolle"
+
+#: views/customer.py:285
+#, python-format
+msgid "Customer %(customer)s moved to %(target)s"
+msgstr "Asiakas %(customer)s siirretty %(target)s kontaktiksi"
+
+#: views/customer.py:307 views/device.py:282 views/order.py:875
+#: views/product.py:325 views/search.py:35
+#, python-format
+msgid "Search results for \"%s\""
+msgstr "Hakutulokset \"%s\""
+
+#: views/customer.py:357
+msgid "Search for customers"
+msgstr "Hae asiakkaita"
+
+#: views/customer.py:376
+msgid "Please specify search query first"
+msgstr ""
+
+#: views/customer.py:419
+#, fuzzy
+msgid "Invalid upload data"
+msgstr "Tarkista asiakastiedot"
+
+#: views/customer.py:439
+#, fuzzy, python-format
+msgid "%d customer(s) imported"
+msgstr "%d laitetta tuotu"
+
+#: views/device.py:114
+msgid "Device deleted"
+msgstr "Laite poistettu"
+
+#: views/device.py:116
+msgid "Cannot delete device with GSX repairs"
+msgstr "Ei voi poistaa laitetta jolla on GSX-korjauksia"
+
+#: views/device.py:314
+#, python-format
+msgid "Devices matching \"%s\""
+msgstr ""
+
+#: views/device.py:333
+msgid "Device search"
+msgstr "Laitehaku"
+
+#: views/device.py:386
+msgid "Invalid serial number for parts lookup"
+msgstr ""
+
+#: views/device.py:389
+msgid "Error calculating prices. Please check your system settings."
+msgstr ""
+
+#: views/device.py:486
+#, python-format
+msgid "%d devices imported"
+msgstr "%d laitetta tuotu"
+
+#: views/device.py:499
+msgid "Warranty status updated successfully"
+msgstr ""
+
+#: views/error.py:24
+#, python-format
+msgid "Browser: %s"
+msgstr "Selain: %s"
+
+#: views/gsx.py:21
+#, python-format
+msgid "Part %s updated"
+msgstr "Varaosa %s päivitetty"
+
+#: views/gsx.py:56
+#, python-format
+msgid "Part %(part)s added to repair %(repair)s"
+msgstr "Varaosa %(part)s lisätty korjaukseen %(repair)s"
+
+#: views/gsx.py:85
+#, python-format
+msgid "Part %(part)s removed from %(repair)s"
+msgstr "Varaosa %(part)s poistettu korjauksesta %(repair)s"
+
+#: views/gsx.py:99
+msgid "Submitted repairs cannot be deleted"
+msgstr "Lähetettyjä GSX korjauksia ei voi poistaa"
+
+#: views/gsx.py:105
+msgid "GSX repair deleted"
+msgstr "GSX korjaus poistettu"
+
+#: views/gsx.py:147
+#, fuzzy
+msgid "Submitted repairs cannot be edited"
+msgstr "Suljettuja tilauksia ei voi muokata"
+
+#: views/gsx.py:150
+msgid "Please add some parts before creating repair"
+msgstr "Tilaus ei sisällä sopivia osia"
+
+#: views/gsx.py:153
+#, fuzzy
+msgid "Cannot create GSX repair without valid customer data"
+msgstr "Ei voida luoda GSX korjausta ilman asiakastietoja"
+
+#: views/gsx.py:190
+msgid "GSX repair saved"
+msgstr "GSX korjaus tallennettu"
+
+#: views/gsx.py:216
+#, fuzzy
+msgid "Invalid component data"
+msgstr "Tarkista asiakastiedot"
+
+#: views/gsx.py:231
+msgid "Invalid customer info"
+msgstr "Tarkista asiakastiedot"
+
+#: views/gsx.py:296
+#, python-format
+msgid "%s serial numbers updated"
+msgstr ""
+
+#: views/invoices.py:91
+#, python-format
+msgid "Receipt #%d"
+msgstr "Kuitti #%d"
+
+#: views/invoices.py:99
+#, python-format
+msgid "Invoice %s"
+msgstr "Lasku %s"
+
+#: views/invoices.py:110
+#, python-format
+msgid "Dispatch Order %s"
+msgstr "Toimita tilaus %s"
+
+#: views/invoices.py:135
+msgid "Walk-In Customer"
+msgstr "Käteisasiakas"
+
+#: views/note.py:170
+#, python-format
+msgid "Re: %s"
+msgstr "Vas: %s"
+
+#: views/note.py:237
+msgid "Note deleted"
+msgstr "Merkintä poistettu"
+
+#: views/note.py:305
+msgid "OK"
+msgstr ""
+
+#: views/note.py:329
+#, python-format
+msgid "Notes containing \"%s\""
+msgstr "Merkinnät jotka sisältää \"%s\""
+
+#: views/note.py:373
+msgid "Message search"
+msgstr "Hae viestejä"
+
+#: views/note.py:384
+#, fuzzy
+msgid "Edit Escalation"
+msgstr "Muokkaa GSX-tiliä"
+
+#: views/order.py:142
+#, python-format
+msgid "%d search results"
+msgstr "%d hakutulosta"
+
+#: views/order.py:153
+#, python-format
+msgid "Order %s"
+msgstr "Tilaus %s"
+
+#: views/order.py:212
+#, python-format
+msgid "Order %s reopened"
+msgstr "Tilaus %s avattu"
+
+#: views/order.py:349
+#, python-format
+msgid "Repair %s marked complete."
+msgstr "Korjaus %s merkitty valmiiksi."
+
+#: views/order.py:404
+#, python-format
+msgid "Order %s deleted"
+msgstr "Tilaus %s poistettu"
+
+#: views/order.py:408
+#, fuzzy, python-format
+msgid "Cannot delete order %(order)s: %(error)s"
+msgstr "Ei voida poistaa tilausta %s: %s"
+
+#: views/order.py:418
+msgid "Follow"
+msgstr "Seuraa tilausta"
+
+#: views/order.py:424
+msgid "Unfollow"
+msgstr "Älä seuraa tilausta"
+
+#: views/order.py:461
+msgid "Closed orders cannot be updated"
+msgstr "Suljettuja tilauksia ei voi muokata"
+
+#: views/order.py:475
+#, python-format
+msgid "User %s does not exist"
+msgstr "Käyttäjää %s ei ole olemassa"
+
+#: views/order.py:503
+#, python-format
+msgid "Label %s does not exist"
+msgstr "Merkkiä %s ei ole määritelty"
+
+#: views/order.py:508 views/order.py:514 views/order.py:523
+msgid "Order updated"
+msgstr "Tilaus päivitetty"
+
+#: views/order.py:520
+#, python-format
+msgid "Order moved to %s"
+msgstr "Tilaus siirretty paikkaan %s"
+
+#: views/order.py:536
+#, python-format
+msgid "Service Order #%s"
+msgstr "Huoltotilaus #%s"
+
+#: views/order.py:633
+#, python-format
+msgid "Products of order %s reserved"
+msgstr "Tilauksen %s tuotteet varattu"
+
+#: views/order.py:674 views/product.py:271
+#, python-format
+msgid "Product %s saved"
+msgstr "Tuote %s tallennettu"
+
+#: views/order.py:761
+msgid "Order item does not exist"
+msgstr "Tilausriviä ei löytynyt"
+
+#: views/order.py:842
+#, python-format
+msgid "Customer %s removed"
+msgstr "Asiakas %s poistettu"
+
+#: views/product.py:117
+msgid "Parts database uploaded for processing"
+msgstr "Varaosatiedot lähetetty käsiteltäväksi"
+
+#: views/product.py:206
+#, python-format
+msgid "%d products imported"
+msgstr "Tuotu %d tuotetta"
+
+#: views/product.py:211
+msgid "Upload products"
+msgstr "Lähetä tuotteet"
+
+#: views/product.py:274
+msgid "Error in inventory details"
+msgstr "Virhe varastosaldoissa"
+
+#: views/product.py:276
+msgid "Error in product info"
+msgstr "Virhe tuotetiedoissa"
+
+#: views/product.py:296
+msgid "Product deleted"
+msgstr "Tuote poistettu"
+
+#: views/product.py:298
+msgid "Cannot delete product"
+msgstr "Tuotteen poistaminen epäonnistui"
+
+#: views/product.py:371
+#, python-format
+msgid "Category %s already exists"
+msgstr "Tuotaryhmä %s on jo olemassa"
+
+#: views/product.py:373
+#, python-format
+msgid "Category %s saved"
+msgstr "Tuotaryhmä %s tallennettu"
+
+#: views/product.py:389
+msgid "Category deleted"
+msgstr "Tuoteryhmä poistettu"
+
+#: views/product.py:437
+msgid "Price info updated from GSX"
+msgstr "Hintatiedot päivitetty"
+
+#: views/product.py:439
+msgid "Failed to update price from GSX"
+msgstr "Hintatietojen päivitys ei onnistunut"
+
+#: views/purchases.py:81
+#, python-format
+msgid "Product %s removed"
+msgstr "Tuote %s poistettu"
+
+#: views/purchases.py:96
+#, python-format
+msgid "Purchase Order %d"
+msgstr "Ostotilaus #%d"
+
+#: views/purchases.py:132
+#, fuzzy, python-format
+msgid "Purchase Order %d saved"
+msgstr "Ostotilaus tallennettu"
+
+#: views/purchases.py:136
+#, fuzzy, python-format
+msgid "Purchase Order %d submitted"
+msgstr "Ostotilaus lähetetty"
+
+#: views/purchases.py:144
+#, python-format
+msgid "Purchase Order #%d"
+msgstr "Ostotilaus #%d"
+
+#: views/purchases.py:159
+#, python-format
+msgid "Purchase Order %s has already been submitted"
+msgstr "Ostotilaus %s on jo vahvistettu"
+
+#: views/purchases.py:177
+#, python-format
+msgid "Products ordered with confirmation %s"
+msgstr "Tuotteet tilattu tilausvahvistuksella %s"
+
+#: views/purchases.py:193
+#, python-format
+msgid "Purchase Order %s deleted"
+msgstr "Ostotilaus %s poistettu"
+
+#: views/shipments.py:83
+#, python-format
+msgid "%d incoming products"
+msgstr "%d saapuvaa tuotetta"
+
+#: views/shipments.py:113
+#, python-format
+msgid "%d products received"
+msgstr "%d tuotetta saavutettu"
+
+#: views/shipments.py:152
+#, python-format
+msgid "Product %s received"
+msgstr "Tuote %s saavutettu"
+
+#: views/shipments.py:186
+msgid "Browse Bulk Returns"
+msgstr "Selaa palautuksia"
+
+#: views/shipments.py:210
+msgid "View bulk return"
+msgstr "Avaa palautuslista"
+
+#: views/shipments.py:223
+#, python-format
+msgid "Location %s has no Ship-To"
+msgstr "Sijainnilla %s ei ole ship-to numeroa"
+
+#: views/shipments.py:246
+msgid "Bulk return saved"
+msgstr "Palautuslista tallennettu"
+
+#: views/shipments.py:251
+#, python-format
+msgid "Bulk return %s submitted"
+msgstr "Palautuslista %s lähetetty"
+
+#: views/shipments.py:266
+#, python-format
+msgid "%d parts pending return"
+msgstr "%d palautettavaa varaosaa"
+
+#: views/shipments.py:279
+#, python-format
+msgid "Part %s removed from bulk return"
+msgstr "Varaosa %s poistettu palautuslistalta"
+
+#: views/shipments.py:296
+#, python-format
+msgid "Part %s added to return"
+msgstr "Varaosa %s lisätty palautuslistalle"
+
+#: views/shipments.py:322
+msgid "Return DOA Part"
+msgstr "Palauta DOA osa"
+
+#: views/shipments.py:331
+msgid "This part will be converted to regular inventory"
+msgstr ""
+
+#: views/shipments.py:346
+msgid "Part updated"
+msgstr "Varaosa päivitetty"
+
+#: views/stats.py:241
+msgid "Time Scale"
+msgstr ""
+
+#: views/stats.py:465
+msgid "No Queue"
+msgstr "Ilman jonoa"
+
+#, fuzzy
+#~ msgid "Error in SMS gateway settings"
+#~ msgstr "Virhe käyttäjätiedoissa"
+
+#~ msgid "No device"
+#~ msgstr "Ei laitetta"
+
+#~ msgid "just now"
+#~ msgstr "hetki sitten"
+
+#~ msgid "today at %(time)s"
+#~ msgstr "Tänään klo %(time)s"
+
+#~ msgid "yesterday at %(time)s"
+#~ msgstr "Eilen klo %(time)s"
+
+#~ msgid "%a, %d.%m at %k:%M"
+#~ msgstr "%a, %d.%m klo %k:%M"
diff --git a/servo/locale/sv_SE/LC_MESSAGES/django.mo b/servo/locale/sv_SE/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..5c78e77
--- /dev/null
+++ b/servo/locale/sv_SE/LC_MESSAGES/django.mo
Binary files differ
diff --git a/servo/locale/sv_SE/LC_MESSAGES/django.po b/servo/locale/sv_SE/LC_MESSAGES/django.po
new file mode 100644
index 0000000..99050d5
--- /dev/null
+++ b/servo/locale/sv_SE/LC_MESSAGES/django.po
@@ -0,0 +1,5299 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-08-23 10:41+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms/account.py:36 forms/account.py:70 forms/account.py:78
+#: forms/admin.py:364 forms/admin.py:407 forms/admin.py:435
+#: models/account.py:138 models/common.py:197
+#: templates/devices/get_info.html:33 templates/devices/summary.html:24
+#: templates/orders/devices.html:41
+msgid "Password"
+msgstr "Lösenord"
+
+#: forms/account.py:41 templates/checkin/confirmation.html:6
+#: templates/orders/repair.html:28 templates/products/receive_item.html:33
+#: templates/purchases/list_pos.html:55 templates/purchases/view_po.html:23
+#: templates/shipments/list_incoming.html:19
+msgid "Confirmation"
+msgstr "Bekräftelse"
+
+#: forms/account.py:54
+msgid "Password and confirmation do not match!"
+msgstr ""
+
+#: forms/account.py:61
+msgid "File size of photo is too large"
+msgstr ""
+
+#: forms/account.py:67
+msgid "First Name"
+msgstr "Förnamn"
+
+#: forms/account.py:68
+msgid "Last Name"
+msgstr "Efternamn"
+
+#: forms/account.py:69 templates/accounts/print_calendar.html:23
+#: templates/orders/print_confirmation.html:24
+#: templates/orders/print_confirmation.html:54
+#: templates/orders/print_receipt.html:12
+msgid "Email Address"
+msgstr "E-postadress"
+
+#: forms/account.py:75 forms/admin.py:358 forms/admin.py:403
+#: forms/admin.py:431 templates/devices/get_info.html:29
+#: templates/devices/summary.html:20 templates/orders/devices.html:37
+msgid "Username"
+msgstr "Användar"
+
+#: forms/admin.py:43 views/product.py:173
+msgid "Unsupported file encoding"
+msgstr ""
+
+#: forms/admin.py:90
+msgid "Group members"
+msgstr ""
+
+#: forms/admin.py:97 templates/admin/users/form.html:9
+msgid "Permissions"
+msgstr ""
+
+#: forms/admin.py:133
+msgid "A location with that name already exists"
+msgstr ""
+
+#: forms/admin.py:192
+#, python-format
+msgid "\"%s\" cannot be used as a username"
+msgstr ""
+
+#: forms/admin.py:234
+#, python-format
+msgid "Syntax error in template: %s"
+msgstr ""
+
+#: forms/admin.py:241
+msgid "Company Name"
+msgstr "Företag"
+
+#: forms/admin.py:243
+#, fuzzy
+msgid "Company Logo"
+msgstr "Företag"
+
+#: forms/admin.py:245
+msgid "Company-wide logo to use in print templates"
+msgstr ""
+
+#: forms/admin.py:250
+msgid "Terms of Service"
+msgstr "Servicevillkor"
+
+#: forms/admin.py:252
+msgid ""
+"These terms will be added to your work confirmations and public check-in "
+"site."
+msgstr ""
+
+#: forms/admin.py:258
+msgid "Autocomplete GSX repairs"
+msgstr ""
+
+#: forms/admin.py:259
+msgid "Complete the GSX repair when closing a Service Order"
+msgstr ""
+
+#: forms/admin.py:264 forms/admin.py:371
+msgid "User Account"
+msgstr ""
+
+#: forms/admin.py:266
+msgid "User account to use for the public check-in service"
+msgstr ""
+
+#: forms/admin.py:270 templates/admin/users/groups.html:12 views/stats.py:276
+msgid "Group"
+msgstr ""
+
+#: forms/admin.py:272
+msgid "Users to choose from in the check-in interface"
+msgstr ""
+
+#: forms/admin.py:276 models/repair.py:54
+msgid "Checklist"
+msgstr ""
+
+#: forms/admin.py:278
+msgid "Checklist to show during check-in"
+msgstr ""
+
+#: forms/admin.py:282 models/queue.py:174
+msgid "Queue"
+msgstr ""
+
+#: forms/admin.py:284
+msgid "Orders created through the check-in interface will go into this queue"
+msgstr ""
+
+#: forms/admin.py:289
+msgid "Show timeline"
+msgstr ""
+
+#: forms/admin.py:290
+msgid "Show status timeline on public repair status page"
+msgstr ""
+
+#: forms/admin.py:295
+#, fuzzy
+msgid "Show password"
+msgstr "Lösenord"
+
+#: forms/admin.py:296
+msgid "Make checkin device password field readable"
+msgstr ""
+
+#: forms/admin.py:301
+msgid "Show checklist results"
+msgstr ""
+
+#: forms/admin.py:302
+msgid "Show checklist results in order confirmation"
+msgstr ""
+
+#: forms/admin.py:306
+msgid "Currency"
+msgstr ""
+
+#: forms/admin.py:319
+msgid "Default GSX account"
+msgstr ""
+
+#: forms/admin.py:321
+msgid "Use this GSX account before and order is assigned to a queue"
+msgstr ""
+
+#: forms/admin.py:327 models/product.py:107 models/product.py:134
+msgid "Margin %"
+msgstr ""
+
+#: forms/admin.py:328
+msgid "Default margin for new products"
+msgstr ""
+
+#: forms/admin.py:334 models/product.py:88
+msgid "VAT %"
+msgstr ""
+
+#: forms/admin.py:335
+msgid "Default VAT for new products"
+msgstr ""
+
+#: forms/admin.py:341
+msgid "Shipping Cost"
+msgstr ""
+
+#: forms/admin.py:342
+msgid "Default shipping cost for new products"
+msgstr ""
+
+#: forms/admin.py:348
+msgid "Track inventory"
+msgstr ""
+
+#: forms/admin.py:349
+msgid "Unchecking this will disable tracking product amounts in your inventory"
+msgstr ""
+
+#: forms/admin.py:353
+msgid "IMAP server"
+msgstr ""
+
+#: forms/admin.py:368 forms/admin.py:410 forms/admin.py:445
+msgid "Use SSL"
+msgstr ""
+
+#: forms/admin.py:373
+msgid "User account to use when creating notes from messages"
+msgstr ""
+
+#: forms/admin.py:378
+msgid "Default Sender"
+msgstr ""
+
+#: forms/admin.py:380 models/account.py:241
+#, fuzzy
+msgid "User"
+msgstr "Användar"
+
+#: forms/admin.py:381 forms/returns.py:53 models/common.py:507
+#: templates/admin/users/form.html:10 views/stats.py:265
+msgid "Location"
+msgstr ""
+
+#: forms/admin.py:382
+msgid "Custom..."
+msgstr ""
+
+#: forms/admin.py:384
+msgid "Select the default sender address for outgoing emails"
+msgstr ""
+
+#: forms/admin.py:396
+msgid "Default subject"
+msgstr ""
+
+#: forms/admin.py:401
+msgid "SMTP server"
+msgstr ""
+
+#: forms/admin.py:413
+msgid "SMS Gateway"
+msgstr ""
+
+#: forms/admin.py:415
+msgid "Built-in"
+msgstr ""
+
+#: forms/admin.py:423 forms/admin.py:456 forms/checkin.py:100
+msgid "Email address"
+msgstr "E-postadress"
+
+#: forms/admin.py:426
+msgid "URL"
+msgstr ""
+
+#: forms/admin.py:428
+msgid "SMS Server URL"
+msgstr ""
+
+#: forms/admin.py:441
+msgid "Sender"
+msgstr ""
+
+#: forms/admin.py:451
+msgid "Notify locations"
+msgstr ""
+
+#: forms/admin.py:452
+msgid "Daily reports will be sent to the location's email address"
+msgstr ""
+
+#: forms/admin.py:457
+msgid "Send daily reports to this email address"
+msgstr ""
+
+#: forms/admin.py:465
+msgid "Invalid margin %"
+msgstr ""
+
+#: forms/checkin.py:26 models/note.py:54
+#, python-format
+msgid "%s is not a valid phone number"
+msgstr "%s är inte ett giltigt telefonnummer"
+
+#: forms/checkin.py:31
+#, python-format
+msgid "%s is not a valid serial or IMEI number"
+msgstr "%s är inte ett giltigt serienummer eller IMEI-nummer"
+
+#: forms/checkin.py:40 forms/repairs.py:22 models/customer.py:99
+#: templates/customers/view.html:38
+msgid "Country"
+msgstr "Land"
+
+#: forms/checkin.py:46 templates/orders/devices.html:54
+#: templates/orders/print_confirmation.html:79
+msgid "Accessories"
+msgstr "Tillbehör"
+
+#: forms/checkin.py:48
+msgid ""
+"Please list here any accessories you'd like to check in with your device "
+"(cables, power adapters, bags, etc)"
+msgstr "Skriv in tillbehör om du skickar med något mer är bara enheten (ex. laddare, väska mm)"
+
+#: forms/checkin.py:53
+msgid "Proof of Purchase"
+msgstr "Kvitto"
+
+#: forms/checkin.py:54
+msgid "Proof of Purchase is required when setting purchase date manually"
+msgstr "Att bifoga ett kvitto krävs om du ändrar inköpsdatumet"
+
+#: forms/checkin.py:93 forms/repairs.py:15
+msgid "First name"
+msgstr "Förnamn"
+
+#: forms/checkin.py:94 forms/repairs.py:16
+msgid "Last name"
+msgstr "Efternamn"
+
+#: forms/checkin.py:97
+msgid "Company (optional)"
+msgstr "Företag"
+
+#: forms/checkin.py:104
+msgid "Phone number"
+msgstr "Telefonnummer"
+
+#: forms/checkin.py:107 forms/repairs.py:19 models/invoices.py:75
+#: templates/customers/find.html:51 templates/customers/view.html:30
+#: templates/orders/print_confirmation.html:57
+#: templates/orders/print_receipt.html:15
+msgid "Address"
+msgstr "Gatuadress"
+
+#: forms/checkin.py:108 forms/repairs.py:21 templates/customers/find.html:53
+msgid "City"
+msgstr "Ort"
+
+#: forms/checkin.py:109 templates/customers/find.html:52
+msgid "Postal Code"
+msgstr "Postnummer"
+
+#: forms/checkin.py:112
+msgid "Check-in location"
+msgstr "Inlämningsställe"
+
+#: forms/checkin.py:115
+msgid "Choose where you want to leave the device"
+msgstr "Välj om du vill skicka in den med post, bud eller om du vill lämna in den i en av våra butiker"
+
+#: forms/checkin.py:119
+msgid "Check-out location"
+msgstr "Utlämningsställe"
+
+#: forms/checkin.py:122
+msgid "Choose where you want to pick up the device"
+msgstr "Välja om du vill att vi skickar tillbaka den med post, bud eller om du vill hämta i en av våra butiker"
+
+#: forms/checkin.py:124
+msgid ""
+"I agree to the <a href=\"/checkin/terms/\" target=\"_blank\">terms of "
+"service.</a>"
+msgstr ""
+"Jag godkänner <a href=\"/checkin/terms/\" target=\"_blank\">servicevillkoren</a>"
+
+#: forms/checkin.py:139
+msgid "Serial number or IMEI"
+msgstr "Serienummer eller IMEI"
+
+#: forms/checkin.py:146 templates/notes/view_note.html:21
+#: templates/products/receive_item.html:17
+#: templates/repairs/get_details.html:10
+#: templates/search/results/gsx_repair_details.html:6
+#: templates/shipments/list_incoming.html:18
+msgid "Service Order"
+msgstr "Service Order"
+
+#: forms/checkin.py:147
+msgid "Invalid Service Order number"
+msgstr "Ogiltig Service Order nummer"
+
+#: forms/checkin.py:155 templates/checkin/issue.html:7
+msgid "Problem description"
+msgstr "Problembeskrivning"
+
+#: forms/checkin.py:160 templates/orders/repair.html:23
+msgid "Attachment"
+msgstr "Bifoga fil"
+
+#: forms/checkin.py:161
+msgid "Please use this to attach relevant documents"
+msgstr "Här kan du ladda upp relevant dokumentation"
+
+#: forms/checkin.py:166 models/common.py:457
+#: templates/accounts/print_calendar.html:45
+#: templates/accounts/view_calendar.html:32 templates/customers/view.html:42
+#: templates/devices/get_info.html:41
+#: templates/devices/search_gsx_warranty.html:48
+#: templates/devices/summary.html:28 templates/notes/list_notes.html:74
+#: templates/orders/devices.html:49 templates/orders/repair.html:32
+#: templates/repairs/get_details.html:16 templates/search/spotlight.html:27
+#: templates/search/results/gsx_repair_details.html:12
+msgid "Notes"
+msgstr "Anmärkningar"
+
+#: forms/checkin.py:168
+msgid "Will not appear on the print-out"
+msgstr ""
+
+#: forms/customer.py:35
+msgid "Enter a valid phone number"
+msgstr "Skriv in ett giltigt telefonnummer"
+
+#: forms/customer.py:44 forms/product.py:17
+msgid "Name contains"
+msgstr "Namnet innehåller"
+
+#: forms/customer.py:48
+msgid "Email contains"
+msgstr "E-post innehåller"
+
+#: forms/customer.py:52
+msgid "Address contains"
+msgstr "Adress innehåller"
+
+#: forms/customer.py:56
+#, fuzzy
+msgid "Checked in between"
+msgstr "Mottagen av"
+
+#: forms/customer.py:72
+msgid "Please specify at least one parameter"
+msgstr "Vänligen ange minst en parameter"
+
+#: forms/customer.py:76
+msgid "CSV file"
+msgstr ""
+
+#: forms/customer.py:80
+msgid "Skip duplicates"
+msgstr ""
+
+#: forms/customer.py:81
+msgid "Skip customers with existing email addresses"
+msgstr ""
+
+#: forms/customer.py:87
+msgid "Data file should be in text format"
+msgstr ""
+
+#: forms/devices.py:26 forms/orders.py:97
+#, fuzzy
+msgid "Created between"
+msgstr "Mottagen av"
+
+#: forms/devices.py:34
+#, fuzzy
+msgid "Serial number contains"
+msgstr "Serienummer eller IMEI"
+
+#: forms/devices.py:42 forms/product.py:25
+msgid "Description contains"
+msgstr "Arbetsbeskrivning innehåller"
+
+#: forms/devices.py:61
+msgid "Serial numbers may not include spaces"
+msgstr "Serienummer får inte innehålla mellanslag"
+
+#: forms/invoices.py:39 forms/orders.py:57 forms/product.py:125
+msgid "State is"
+msgstr ""
+
+#: forms/invoices.py:41 forms/invoices.py:50 forms/product.py:127
+msgid "Any"
+msgstr "Alla"
+
+#: forms/invoices.py:42 forms/product.py:128 models/order.py:128
+msgid "Open"
+msgstr ""
+
+#: forms/invoices.py:43
+msgid "Paid"
+msgstr "Öppen"
+
+#: forms/invoices.py:49
+msgid "Payment method is"
+msgstr ""
+
+#: forms/invoices.py:55
+msgid "Status is not"
+msgstr "Status är inte"
+
+#: forms/invoices.py:61 forms/invoices.py:64 forms/product.py:140
+#: forms/product.py:143 forms/product.py:173 forms/product.py:187
+#: views/stats.py:250
+msgid "Start date"
+msgstr ""
+
+#: forms/invoices.py:69 forms/invoices.py:72 forms/product.py:148
+#: forms/product.py:151 forms/product.py:180 forms/product.py:194
+#: views/stats.py:255
+msgid "End date"
+msgstr ""
+
+#: forms/invoices.py:77
+msgid "Customer name contains"
+msgstr "Kundnamn innehåller"
+
+#: forms/invoices.py:81 forms/notes.py:43
+msgid "Service Order is"
+msgstr ""
+
+#: forms/notes.py:29 models/note.py:81 templates/notes/view_note.html:19
+msgid "From"
+msgstr ""
+
+#: forms/notes.py:40
+msgid "Body contains"
+msgstr ""
+
+#: forms/notes.py:41
+msgid "Recipient contains"
+msgstr ""
+
+#: forms/notes.py:42
+msgid "Sender contains"
+msgstr ""
+
+#: forms/orders.py:52 forms/product.py:165
+msgid "Location is"
+msgstr ""
+
+#: forms/orders.py:62
+msgid "Queue is"
+msgstr ""
+
+#: forms/orders.py:67 models/queue.py:236 templates/admin/queues/form.html:27
+#: templates/devices/search_gsx_repairs.html:9
+#: templates/invoices/index.html:47 templates/orders/list.html:14
+#: templates/repairs/get_details.html:20
+#: templates/search/results/gsx_repair_details.html:16 views/stats.py:286
+msgid "Status"
+msgstr ""
+
+#: forms/orders.py:72
+msgid "Created by"
+msgstr ""
+
+#: forms/orders.py:77 templates/orders/list.html:13
+msgid "Assigned to"
+msgstr ""
+
+#: forms/orders.py:82 templates/orders/edit.html:91
+msgid "Label"
+msgstr ""
+
+#: forms/orders.py:87 models/common.py:345
+msgid "Green"
+msgstr ""
+
+#: forms/orders.py:88
+msgid "Yellow"
+msgstr ""
+
+#: forms/orders.py:89 models/common.py:347
+msgid "Red"
+msgstr ""
+
+#: forms/orders.py:90
+msgid "Grey"
+msgstr ""
+
+#: forms/orders.py:92
+msgid "Color"
+msgstr ""
+
+#: forms/product.py:21
+#, fuzzy
+msgid "Code contains"
+msgstr "Kundsnamn"
+
+#: forms/product.py:29
+msgid "Device model is"
+msgstr ""
+
+#: forms/product.py:40
+msgid "Product datafile"
+msgstr ""
+
+#: forms/product.py:48
+msgid "Parts database file"
+msgstr ""
+
+#: forms/product.py:52
+msgid "Import vintage parts"
+msgstr ""
+
+#: forms/product.py:57
+msgid "Update product prices"
+msgstr ""
+
+#: forms/product.py:112
+#, python-format
+msgid "Product code %s contains invalid characters"
+msgstr ""
+
+#: forms/product.py:129
+msgid "Submitted"
+msgstr ""
+
+#: forms/product.py:130 templates/products/receive_item.html:26
+#: templates/products/view.html:158 templates/purchases/list_pos.html:56
+msgid "Received"
+msgstr ""
+
+#: forms/product.py:156
+#, fuzzy
+msgid "Reference contains"
+msgstr "Kundsnamn"
+
+#: forms/product.py:170
+msgid "Ordered between"
+msgstr ""
+
+#: forms/product.py:184
+#, fuzzy
+msgid "Received between"
+msgstr "Mottagen av"
+
+#: forms/product.py:198
+#, fuzzy
+msgid "Confirmation is"
+msgstr "Bekräftelse"
+
+#: forms/product.py:201
+msgid "Service order is"
+msgstr ""
+
+#: forms/repairs.py:17 models/invoices.py:69 templates/customers/find.html:49
+#: templates/customers/view.html:22
+msgid "Email"
+msgstr ""
+
+#: forms/repairs.py:18 models/invoices.py:63
+#: templates/accounts/print_calendar.html:24 templates/customers/find.html:50
+#: templates/customers/search.html:18 templates/customers/view.html:26
+#: templates/orders/print_confirmation.html:25
+#: templates/search/results/customers.html:14
+msgid "Phone"
+msgstr "Telefon"
+
+#: forms/repairs.py:20 models/common.py:413 models/customer.py:88
+msgid "ZIP Code"
+msgstr "Postnummer"
+
+#: forms/repairs.py:23 models/common.py:325 models/product.py:224
+msgid "Other"
+msgstr ""
+
+#: forms/repairs.py:58 models/repair.py:103
+msgid "Technician"
+msgstr ""
+
+#: forms/repairs.py:73
+msgid "Replacement serial number must be set"
+msgstr ""
+
+#: forms/returns.py:16
+msgid "Reason"
+msgstr ""
+
+#: forms/returns.py:18 forms/returns.py:29 forms/returns.py:39
+msgid "Select..."
+msgstr ""
+
+#: forms/returns.py:19
+msgid "Part not needed"
+msgstr ""
+
+#: forms/returns.py:20
+msgid "Duplicated part"
+msgstr ""
+
+#: forms/returns.py:21
+msgid "Added wrong part"
+msgstr ""
+
+#: forms/returns.py:22
+msgid "Tried to cancel order"
+msgstr ""
+
+#: forms/returns.py:23
+msgid "Customer refused order"
+msgstr ""
+
+#: forms/returns.py:27
+msgid "Type"
+msgstr ""
+
+#: forms/returns.py:30
+msgid "Diagnostic"
+msgstr ""
+
+#: forms/returns.py:31
+msgid "Un-Opened"
+msgstr ""
+
+#: forms/returns.py:38 models/parts.py:46
+msgid "Symptom Code"
+msgstr ""
+
+#: forms/returns.py:42 models/parts.py:51
+msgid "Symptom Modifier"
+msgstr ""
+
+#: management/commands/cron.py:81 models/order.py:127
+msgid "Unassigned"
+msgstr ""
+
+#: management/commands/cron.py:82
+msgid "No Status"
+msgstr ""
+
+#: management/commands/cron.py:86
+#, python-format
+msgid "Repairs aging beyond limits at %s"
+msgstr ""
+
+#: management/commands/cron.py:115
+msgid "Products stocked below limit"
+msgstr ""
+
+#: messaging/sms.py:14
+msgid "SMS sender not configured"
+msgstr ""
+
+#: messaging/sms.py:51
+msgid "SMS sender name not configured"
+msgstr ""
+
+#: messaging/sms.py:77
+#, python-format
+msgid "Failed to send message to %s"
+msgstr ""
+
+#: messaging/sms.py:90
+msgid "Wrong sender name"
+msgstr ""
+
+#: messaging/sms.py:91
+msgid "Too many messages in one request"
+msgstr ""
+
+#: messaging/sms.py:92
+msgid "Invalid username or password"
+msgstr ""
+
+#: messaging/sms.py:93
+msgid "Insufficient credits on your account"
+msgstr ""
+
+#: messaging/sms.py:94
+msgid "Unsuccessful message submission"
+msgstr ""
+
+#: messaging/sms.py:95 messaging/sms.py:96
+msgid "Internal system error"
+msgstr ""
+
+#: messaging/sms.py:118
+#, python-format
+msgid "Unknown error (%s)"
+msgstr ""
+
+#: messaging/sms.py:132
+msgid "No SMS HTTP gateway defined"
+msgstr ""
+
+#: models/account.py:53 templates/admin/users/index.html:7
+msgid "New User"
+msgstr ""
+
+#: models/account.py:64 templates/orders/edit.html:33
+msgid "Current Location"
+msgstr ""
+
+#: models/account.py:65
+msgid "Orders you create will be registered to this location."
+msgstr ""
+
+#: models/account.py:71
+msgid "queues"
+msgstr ""
+
+#: models/account.py:74
+msgid "Danish"
+msgstr ""
+
+#: models/account.py:75
+msgid "Dutch"
+msgstr ""
+
+#: models/account.py:76
+msgid "English"
+msgstr ""
+
+#: models/account.py:77
+msgid "Estonian"
+msgstr ""
+
+#: models/account.py:78
+msgid "Finnish"
+msgstr ""
+
+#: models/account.py:79
+msgid "Swedish"
+msgstr ""
+
+#: models/account.py:85
+msgid "language"
+msgstr ""
+
+#: models/account.py:86
+msgid "Select which language you want to use Servo in."
+msgstr ""
+
+#: models/account.py:94 models/common.py:428
+#, fuzzy
+msgid "Time zone"
+msgstr "Telefon"
+
+#: models/account.py:98
+msgid "Denmark"
+msgstr ""
+
+#: models/account.py:99
+msgid "Estonia"
+msgstr ""
+
+#: models/account.py:100
+msgid "Finland"
+msgstr ""
+
+#: models/account.py:101
+msgid "United States"
+msgstr ""
+
+#: models/account.py:102
+msgid "Netherlands"
+msgstr ""
+
+#: models/account.py:103
+msgid "Sweden"
+msgstr ""
+
+#: models/account.py:109
+msgid "region"
+msgstr ""
+
+#: models/account.py:110
+msgid "Affects formatting of numbers, dates and currencies."
+msgstr ""
+
+#: models/account.py:114
+msgid "email notifications"
+msgstr ""
+
+#: models/account.py:115
+msgid "Event notifications will also be emailed to you."
+msgstr ""
+
+#: models/account.py:119
+msgid "print automatically"
+msgstr ""
+
+#: models/account.py:120
+msgid "Opens print dialog automatically."
+msgstr ""
+
+#: models/account.py:126
+msgid "tech ID"
+msgstr ""
+
+#: models/account.py:132 models/common.py:190
+msgid "User ID"
+msgstr ""
+
+#: models/account.py:144
+msgid "PO prefix"
+msgstr ""
+
+#: models/account.py:145
+msgid "GSX repairs you create will be prefixed"
+msgstr ""
+
+#: models/account.py:152 models/customer.py:107 models/device.py:152
+#: models/product.py:202
+msgid "photo"
+msgstr ""
+
+#: models/account.py:153
+msgid "Maximum avatar size is 1MB"
+msgstr ""
+
+#: models/account.py:242 templates/admin/index.html:24 views/admin.py:405
+msgid "Users & Groups"
+msgstr ""
+
+#: models/calendar.py:25 models/common.py:632 models/common.py:709
+#: models/order.py:704 models/repair.py:32
+msgid "title"
+msgstr ""
+
+#: models/calendar.py:26 templates/accounts/calendars.html:7
+msgid "New Calendar"
+msgstr ""
+
+#: models/calendar.py:31
+msgid "hours per day"
+msgstr ""
+
+#: models/calendar.py:32
+msgid "How many hours per day should be in this calendar"
+msgstr ""
+
+#: models/calendar.py:49
+#, python-format
+msgid ""
+"%(hours)s hours total in %(workdays)s days (%(overtime)s hours overtime)."
+msgstr ""
+
+#: models/calendar.py:52
+#, python-format
+msgid "%(hours)s hours total in %(workdays)s days."
+msgstr ""
+
+#: models/common.py:176
+msgid "New GSX Account"
+msgstr ""
+
+#: models/common.py:177 models/queue.py:126
+msgid "Sold-To"
+msgstr ""
+
+#: models/common.py:178 models/common.py:444
+msgid "Ship-To"
+msgstr ""
+
+#: models/common.py:183
+msgid "Region"
+msgstr ""
+
+#: models/common.py:202
+msgid "Environment"
+msgstr ""
+
+#: models/common.py:227
+msgid "Default GSX account not configured"
+msgstr ""
+
+#: models/common.py:252
+#, fuzzy
+msgid "Configuration error"
+msgstr "Konfiguration"
+
+#: models/common.py:297 models/escalations.py:25
+msgid "GSX Account"
+msgstr ""
+
+#: models/common.py:298 templates/admin/index.html:12
+msgid "GSX Accounts"
+msgstr ""
+
+#: models/common.py:317 templates/admin/tags/index.html:6
+msgid "New Tag"
+msgstr ""
+
+#: models/common.py:318 models/common.py:393 models/customer.py:25
+#: models/customer.py:59 models/queue.py:197
+msgid "name"
+msgstr ""
+
+#: models/common.py:322 templates/devices/index.html:49
+#: templates/devices/search_gsx.html:8 templates/devices/search_gsx.html:10
+#: templates/devices/search_gsx_error.html:8 templates/devices/view.html:7
+#: templates/search/results/gsx.html:8 templates/search/results/gsx.html:10
+msgid "Device"
+msgstr ""
+
+#: models/common.py:323 models/common.py:625
+#: templates/accounts/updates.html:37 templates/invoices/index.html:44
+#: templates/notes/form.html:109 templates/orders/dispatch.html:105
+#: templates/orders/edit.html:114 templates/orders/gsx_repair_form.html:89
+#: templates/orders/list.html:10 templates/orders/parts.html:33
+#: templates/orders/tabs.html:4 templates/products/index_outgoing.html:25
+#: templates/products/view.html:129 templates/products/view.html.py:155
+#: templates/products/view.html:180 templates/purchases/list_pos.html:57
+#: templates/shipments/list_returns.html:14
+msgid "Order"
+msgstr "Arbet"
+
+#: models/common.py:324 templates/notes/form.html:37
+msgid "Note"
+msgstr ""
+
+#: models/common.py:331 models/common.py:639
+msgid "type"
+msgstr ""
+
+#: models/common.py:344
+msgid "Default"
+msgstr ""
+
+#: models/common.py:346
+msgid "Orange"
+msgstr ""
+
+#: models/common.py:348
+msgid "Blue"
+msgstr ""
+
+#: models/common.py:374 templates/orders/notes.html:32
+msgid "Tag"
+msgstr ""
+
+#: models/common.py:375 templates/admin/index.html:30
+#: templates/devices/form.html:25 templates/products/form.html:51
+msgid "Tags"
+msgstr ""
+
+#: models/common.py:394 templates/admin/locations/index.html:5
+msgid "New Location"
+msgstr ""
+
+#: models/common.py:400 models/customer.py:71
+msgid "phone"
+msgstr "telefon"
+
+#: models/common.py:402 models/customer.py:76
+msgid "email"
+msgstr ""
+
+#: models/common.py:407 models/customer.py:82
+msgid "address"
+msgstr ""
+
+#: models/common.py:419 models/customer.py:94
+msgid "city"
+msgstr ""
+
+#: models/common.py:437
+msgid "Accounts"
+msgstr ""
+
+#: models/common.py:450
+msgid "Timezone"
+msgstr ""
+
+#: models/common.py:458
+msgid "Will be shown on print templates"
+msgstr ""
+
+#: models/common.py:465
+msgid "Logo"
+msgstr ""
+
+#: models/common.py:470
+msgid "Enabled"
+msgstr ""
+
+#: models/common.py:508 templates/admin/index.html:15
+#: templates/stats/index.html:9 templates/stats/locations.html:6
+#: templates/stats/queues.html:6 templates/stats/sales.html:6
+#: templates/stats/statuses.html:6 views/admin.py:510
+msgid "Locations"
+msgstr ""
+
+#: models/common.py:624 templates/customers/view.html:7
+#: templates/devices/search_gsx_repairs.html:8
+#: templates/invoices/index.html:45 templates/orders/dispatch.html:20
+#: templates/orders/gsx_repair_form.html:44 templates/orders/list.html:11
+#: templates/products/view.html:130
+msgid "Customer"
+msgstr "Kund"
+
+#: models/common.py:626 templates/orders/dispatch.html:63
+#: templates/orders/print_confirmation.html:105
+#: templates/orders/print_dispatch.html:13 templates/orders/products.html:10
+#: templates/products/list.html:7 templates/products/view.html:43
+#: templates/purchases/edit_po.html:32 templates/purchases/view_po.html:31
+msgid "Product"
+msgstr ""
+
+#: models/common.py:631 templates/admin/fields/index.html:6
+msgid "New Field"
+msgstr ""
+
+#: models/common.py:645
+msgid "format"
+msgstr ""
+
+#: models/common.py:647
+msgid "value"
+msgstr ""
+
+#: models/common.py:664
+msgid "Field"
+msgstr ""
+
+#: models/common.py:665 templates/admin/index.html:27
+msgid "Fields"
+msgstr ""
+
+#: models/common.py:710 templates/admin/templates/list_templates.html:6
+msgid "New Template"
+msgstr ""
+
+#: models/common.py:713
+msgid "content"
+msgstr ""
+
+#: models/common.py:732
+msgid "Template"
+msgstr ""
+
+#: models/common.py:733 templates/admin/index.html:33
+#: templates/admin/queues/form.html:13
+msgid "Templates"
+msgstr ""
+
+#: models/common.py:741
+msgid "file"
+msgstr ""
+
+#: models/common.py:761
+#, python-format
+msgid "%s is not of an allowed file type"
+msgstr ""
+
+#: models/customer.py:24 templates/admin/users/index.html:12
+#: templates/customers/index.html:16
+msgid "New Group"
+msgstr ""
+
+#: models/customer.py:54 models/customer.py:142
+msgid "company"
+msgstr ""
+
+#: models/customer.py:60 templates/customers/choose.html:17
+#: templates/customers/index.html:7 templates/customers/search.html:5
+#: templates/search/spotlight.html:6
+msgid "New Customer"
+msgstr ""
+
+#: models/customer.py:114 templates/admin/users/tabs.html:6
+#: templates/customers/index.html:55
+msgid "Groups"
+msgstr ""
+
+#: models/customer.py:121
+msgid "tags"
+msgstr ""
+
+#: models/customer.py:128 models/device.py:146
+msgid "notes"
+msgstr ""
+
+#: models/customer.py:136
+msgid "devices"
+msgstr ""
+
+#: models/device.py:39 templates/devices/find.html:27
+#: templates/devices/get_info.html:14
+#: templates/devices/search_gsx_warranty.html:15
+#: templates/devices/summary.html:13 templates/orders/devices.html:20
+#: templates/orders/print_confirmation.html:72
+#: templates/orders/products.html:28 templates/products/index_outgoing.html:28
+#: templates/search/results/gsx_warranty.html:15
+#: templates/shipments/list_returns.html:17
+msgid "Serial Number"
+msgstr "Serienummer"
+
+#: models/device.py:43 templates/devices/choose.html:17
+#: templates/devices/index.html:7 templates/devices/index.html.py:10
+msgid "New Device"
+msgstr ""
+
+#: models/device.py:44 models/order.py:708 models/queue.py:44
+#: models/queue.py:202
+msgid "description"
+msgstr "Enhet"
+
+#: models/device.py:49
+msgid "Apple"
+msgstr ""
+
+#: models/device.py:50 models/product.py:183
+msgid "Brand"
+msgstr ""
+
+#: models/device.py:57 templates/orders/print_confirmation.html:68
+msgid "IMEI Number"
+msgstr ""
+
+#: models/device.py:63 templates/devices/search_gsx_warranty.html:24
+#: templates/orders/devices.html:31
+msgid "Initial Activation Policy"
+msgstr ""
+
+#: models/device.py:69 templates/devices/search_gsx_warranty.html:22
+#: templates/orders/devices.html:29
+msgid "Applied Activation Policy"
+msgstr ""
+
+#: models/device.py:79 templates/devices/index.html:26
+msgid "Product Line"
+msgstr ""
+
+#: models/device.py:85
+msgid "Products that are compatible with this device instance"
+msgstr ""
+
+#: models/device.py:92
+msgid "configuration"
+msgstr ""
+
+#: models/device.py:96 models/parts.py:78
+msgid "Apple Limited Warranty"
+msgstr ""
+
+#: models/device.py:97 models/parts.py:82
+msgid "AppleCare Protection Plan"
+msgstr ""
+
+#: models/device.py:98
+msgid "Customer Satisfaction (CS) Code"
+msgstr ""
+
+#: models/device.py:99 models/parts.py:75
+msgid "Custom Bid Contracts"
+msgstr ""
+
+#: models/device.py:100
+msgid "3'rd Party Warranty"
+msgstr ""
+
+#: models/device.py:101
+msgid "Out Of Warranty (No Coverage)"
+msgstr ""
+
+#: models/device.py:102 views/gsx.py:131
+msgid "Unknown"
+msgstr "Okänd"
+
+#: models/device.py:109 templates/checkin/device.html:16
+#: templates/devices/find.html:30 templates/devices/get_info.html:10
+#: templates/devices/search_gsx_warranty.html:11
+#: templates/devices/summary.html:9 templates/orders/devices.html:16
+#: templates/orders/print_confirmation.html:65
+#: templates/search/results/gsx_warranty.html:11
+msgid "Warranty Status"
+msgstr "Garantistatus"
+
+#: models/device.py:115
+msgid "username"
+msgstr ""
+
+#: models/device.py:121
+msgid "password"
+msgstr ""
+
+#: models/device.py:126
+msgid "Date Purchased"
+msgstr "Inköpsdatum"
+
+#: models/device.py:134
+msgid "Purchase Country"
+msgstr ""
+
+#: models/device.py:158
+msgid "Image URL"
+msgstr ""
+
+#: models/device.py:163
+msgid "Manual URL"
+msgstr ""
+
+#: models/device.py:168 templates/devices/view.html:38
+#: templates/devices/view.html.py:40
+msgid "Exploded View"
+msgstr ""
+
+#: models/device.py:285
+#, python-format
+msgid "Invalid argument for warranty check: %s"
+msgstr ""
+
+#: models/device.py:352
+#, python-format
+msgid "SLA Group: %s"
+msgstr ""
+
+#: models/device.py:354
+msgid "This unit is eligible for Onsite Service."
+msgstr ""
+
+#: models/device.py:356
+msgid "Parts and Labor are covered."
+msgstr ""
+
+#: models/escalations.py:96
+#, python-format
+msgid "Escalation %s"
+msgstr ""
+
+#: models/invoices.py:27 models/invoices.py:163
+msgid "No Charge"
+msgstr "Utan kostnad"
+
+#: models/invoices.py:28 models/invoices.py:164
+msgid "Cash"
+msgstr ""
+
+#: models/invoices.py:29 models/invoices.py:165
+#: templates/products/view.html:179
+msgid "Invoice"
+msgstr "Faktura"
+
+#: models/invoices.py:30 models/invoices.py:166
+msgid "Credit Card"
+msgstr "Kreditkort"
+
+#: models/invoices.py:31 models/invoices.py:167
+msgid "Mail payment"
+msgstr ""
+
+#: models/invoices.py:32 models/invoices.py:168
+msgid "Online payment"
+msgstr ""
+
+#: models/invoices.py:39 models/invoices.py:173
+#: templates/invoices/index.html:50
+msgid "Payment Method"
+msgstr ""
+
+#: models/invoices.py:42
+msgid "paid"
+msgstr ""
+
+#: models/invoices.py:56
+msgid "Walk-in"
+msgstr ""
+
+#: models/invoices.py:57 templates/customers/find.html:48
+#: templates/orders/print_confirmation.html:177
+msgid "Name"
+msgstr "Namn"
+
+#: models/invoices.py:81 models/repair.py:127
+#: templates/devices/search_gsx_repairs.html:6 templates/orders/repair.html:30
+#: templates/products/receive_item.html:23
+#: templates/products/receive_item.html:30 templates/purchases/edit_po.html:33
+#: templates/purchases/list_pos.html:54 templates/purchases/view_po.html:21
+#: templates/purchases/view_po.html:32
+#: templates/shipments/edit_bulk_return.html:57
+msgid "Reference"
+msgstr ""
+
+#: models/invoices.py:132
+msgid "Sales Price"
+msgstr ""
+
+#: models/invoices.py:186 views/invoices.py:159
+#, python-format
+msgid "Order %s dispatched"
+msgstr ""
+
+#: models/invoices.py:195
+#, python-format
+msgid "Payment for %0.2f received"
+msgstr ""
+
+#: models/note.py:66
+msgid "subject"
+msgstr ""
+
+#: models/note.py:70
+msgid "Message"
+msgstr ""
+
+#: models/note.py:87
+msgid "To"
+msgstr ""
+
+#: models/note.py:112 models/order.py:763
+msgid "report"
+msgstr ""
+
+#: models/note.py:116
+msgid "read"
+msgstr ""
+
+#: models/note.py:121
+msgid "flagged"
+msgstr ""
+
+#: models/note.py:159
+#, fuzzy, python-format
+msgid "Default Address <%s>"
+msgstr "E-postadress"
+
+#: models/note.py:271
+msgid "As Unreported"
+msgstr ""
+
+#: models/note.py:271
+msgid "As Reported"
+msgstr ""
+
+#: models/note.py:274
+msgid "As Unread"
+msgstr ""
+
+#: models/note.py:274
+msgid "As Read"
+msgstr ""
+
+#: models/note.py:277
+msgid "As Unflagged"
+msgstr ""
+
+#: models/note.py:277
+msgid "As Flagged"
+msgstr ""
+
+#: models/note.py:319 models/note.py:409
+#, python-format
+msgid "Already sent message to %s"
+msgstr ""
+
+#: models/note.py:364 models/note.py:448
+#, python-format
+msgid "Message sent to %s"
+msgstr ""
+
+#: models/note.py:385
+msgid "System is not configured for built-in SMS support."
+msgstr ""
+
+#: models/note.py:417
+msgid "SMS gateway not configured"
+msgstr ""
+
+#: models/note.py:473
+#, python-format
+msgid "Escalation %s created"
+msgstr ""
+
+#: models/note.py:476
+#, python-format
+msgid "Escalation %s updated"
+msgstr ""
+
+#: models/note.py:481
+msgid "Note saved"
+msgstr ""
+
+#: models/order.py:41 models/queue.py:60
+msgid "priority"
+msgstr ""
+
+#: models/order.py:106 models/repair.py:39
+msgid "queue"
+msgstr ""
+
+#: models/order.py:112
+msgid "status"
+msgstr ""
+
+#: models/order.py:129
+msgid "Closed"
+msgstr ""
+
+#: models/order.py:215
+#, python-format
+msgid "%s added"
+msgstr ""
+
+#: models/order.py:219
+msgid "This device has already been added to this order"
+msgstr ""
+
+#: models/order.py:237
+#, python-format
+msgid "%s removed"
+msgstr ""
+
+#: models/order.py:260
+msgid "Closed a moment ago"
+msgstr ""
+
+#: models/order.py:261
+#, python-format
+msgid "Closed for %(time)s"
+msgstr ""
+
+#: models/order.py:265
+#, python-format
+msgid "%s a moment ago"
+msgstr ""
+
+#: models/order.py:268
+#, python-format
+msgid "%(status)s for %(time)s"
+msgstr ""
+
+#: models/order.py:272
+msgid "Created a moment ago"
+msgstr ""
+
+#: models/order.py:273
+#, python-format
+msgid "Unassigned for %(delta)s"
+msgstr ""
+
+#: models/order.py:277
+msgid "Started a moment ago"
+msgstr ""
+
+#: models/order.py:278
+#, python-format
+msgid "Open for %(delta)s"
+msgstr ""
+
+#: models/order.py:281
+msgid "Select place"
+msgstr ""
+
+#: models/order.py:284
+msgid "Select status"
+msgstr ""
+
+#: models/order.py:291
+msgid "Select user"
+msgstr ""
+
+#: models/order.py:294
+msgid "Select queue"
+msgstr ""
+
+#: models/order.py:303 templates/default.html:35
+#: templates/accounts/orders.html:58 templates/accounts/tabs.html:7
+#: templates/customers/view.html:8 templates/devices/view.html:8
+#: templates/orders/index.html:49 templates/search/spotlight.html:22
+#: views/order.py:43
+msgid "Orders"
+msgstr ""
+
+#: models/order.py:312 views/order.py:195
+#, python-format
+msgid "Order %s closed"
+msgstr ""
+
+#: models/order.py:411
+msgid "Order must belong to a queue to set status"
+msgstr ""
+
+#: models/order.py:445
+msgid "Status unassigned"
+msgstr ""
+
+#: models/order.py:475
+msgid "Removed from queue"
+msgstr ""
+
+#: models/order.py:501
+msgid "Order unassigned"
+msgstr ""
+
+#: models/order.py:505
+#, python-format
+msgid "Order %(order)s assigned to %(user)s"
+msgstr ""
+
+#: models/order.py:598 views/order.py:693 views/purchases.py:90
+#, python-format
+msgid "Product %s added"
+msgstr ""
+
+#: models/order.py:604
+#, python-format
+msgid "Product %s removed from order"
+msgstr ""
+
+#: models/order.py:685
+msgid "Can set assignee"
+msgstr ""
+
+#: models/order.py:686
+msgid "Can change status"
+msgstr ""
+
+#: models/order.py:687
+msgid "Can follow order"
+msgstr ""
+
+#: models/order.py:719
+msgid "amount"
+msgstr ""
+
+#: models/order.py:724
+#, fuzzy
+msgid "KGB Serial Number"
+msgstr "Arbetsorder"
+
+#: models/order.py:759
+msgid "dispatched"
+msgstr ""
+
+#: models/order.py:768
+msgid "sales price"
+msgstr ""
+
+#: models/order.py:783
+msgid "KBB Serial Number"
+msgstr ""
+
+#: models/order.py:790 templates/orders/print_confirmation.html:120
+#: templates/orders/print_receipt.html:23
+msgid "IMEI"
+msgstr ""
+
+#: models/order.py:794 templates/orders/print_confirmation.html:106
+#: templates/orders/print_dispatch.html:14
+msgid "Warranty"
+msgstr "Garanti"
+
+#: models/order.py:795 templates/devices/parts.html:11
+#: templates/products/form.html:11 templates/products/get_info.html:23
+#: templates/products/list.html:9 templates/products/view.html:64
+msgid "Exchange Price"
+msgstr ""
+
+#: models/order.py:796 templates/devices/parts.html:10
+#: templates/products/form.html:12 templates/products/get_info.html:21
+#: templates/products/list.html:8 templates/products/view.html:62
+msgid "Stock Price"
+msgstr ""
+
+#: models/order.py:803
+msgid "Price category"
+msgstr "Priskategorin"
+
+#: models/order.py:810
+msgid "symptom code"
+msgstr ""
+
+#: models/order.py:816
+msgid "symptom modifier"
+msgstr ""
+
+#: models/order.py:1005 models/product.py:589
+msgid "New Shipping Method"
+msgstr ""
+
+#: models/order.py:1034 views/order.py:238
+#, python-format
+msgid "Order %s created"
+msgstr ""
+
+#: models/parts.py:76
+#, fuzzy
+msgid "Customer Satisfaction"
+msgstr "Kundinformation"
+
+#: models/parts.py:77
+msgid "DOA Coverage"
+msgstr ""
+
+#: models/parts.py:79
+msgid "Missing Upon First Use"
+msgstr ""
+
+#: models/parts.py:80
+msgid "Out of Warranty (no coverage)"
+msgstr ""
+
+#: models/parts.py:81
+msgid "AppleCare Parts Agreement"
+msgstr ""
+
+#: models/parts.py:83
+msgid "Quality Program"
+msgstr ""
+
+#: models/parts.py:84
+msgid "AppleCare Repair Agreement"
+msgstr ""
+
+#: models/parts.py:85 templates/orders/devices.html:13
+msgid "Repeat Service"
+msgstr ""
+
+#: models/parts.py:86
+msgid "Additional Part Coverage"
+msgstr ""
+
+#: models/parts.py:87
+msgid "Additional Service Coverage"
+msgstr ""
+
+#: models/parts.py:88
+msgid "NEW - AppleCare Protection Plan"
+msgstr ""
+
+#: models/parts.py:89
+msgid "Consumer Law Coverage"
+msgstr ""
+
+#: models/parts.py:231
+msgid "Unregister from Return"
+msgstr ""
+
+#: models/parts.py:233 templates/repairs/part_menu.html:8
+msgid "Register for Return"
+msgstr ""
+
+#: models/parts.py:328
+msgid "GSX repair has no dispatch ID"
+msgstr ""
+
+#: models/product.py:64
+msgid "code"
+msgstr ""
+
+#: models/product.py:71
+msgid "Substituted (new) code of this part"
+msgstr ""
+
+#: models/product.py:76 templates/products/index.html:9
+msgid "New Product"
+msgstr ""
+
+#: models/product.py:77 models/queue.py:27
+#: templates/products/index_outgoing.html:24
+#: templates/shipments/list_returns.html:13
+msgid "Title"
+msgstr ""
+
+#: models/product.py:82 models/repair.py:64 templates/devices/find.html:28
+msgid "Description"
+msgstr "Enhet"
+
+#: models/product.py:93
+msgid "Don't update price when recalculating prices or importing parts"
+msgstr ""
+
+#: models/product.py:100 models/product.py:128
+msgid "Purchase price"
+msgstr ""
+
+#: models/product.py:113 models/product.py:140
+msgid "Net price"
+msgstr ""
+
+#: models/product.py:114 models/product.py:141
+msgid "Purchase price + margin %"
+msgstr ""
+
+#: models/product.py:120 models/product.py:147
+msgid "Sales price"
+msgstr ""
+
+#: models/product.py:121 models/product.py:148
+msgid "Purchase price + margin % + shipping + VAT %"
+msgstr ""
+
+#: models/product.py:153
+msgid "is serialized"
+msgstr ""
+
+#: models/product.py:154
+#, fuzzy
+msgid "Product has a serial number"
+msgstr "Produkter och tjänster"
+
+#: models/product.py:171
+#, fuzzy
+msgid "Warranty (months)"
+msgstr "Garanti"
+
+#: models/product.py:177
+msgid "Shelf code"
+msgstr ""
+
+#: models/product.py:189 templates/admin/inventory/index.html:9
+#: templates/products/index.html:60 templates/products/view.html:10
+msgid "Categories"
+msgstr ""
+
+#: models/product.py:195
+msgid "device models"
+msgstr ""
+
+#: models/product.py:205
+msgid "shipping"
+msgstr ""
+
+#: models/product.py:213
+msgid "component group"
+msgstr ""
+
+#: models/product.py:220
+msgid "Module"
+msgstr ""
+
+#: models/product.py:221
+msgid "Replacement"
+msgstr ""
+
+#: models/product.py:222
+msgid "Service"
+msgstr ""
+
+#: models/product.py:223
+msgid "Service Contract"
+msgstr ""
+
+#: models/product.py:231
+msgid "part type"
+msgstr ""
+
+#: models/product.py:238
+msgid "EEE code"
+msgstr ""
+
+#: models/product.py:252
+#, python-format
+msgid "%d months"
+msgstr ""
+
+#: models/product.py:419
+#, python-format
+msgid "Product %s not found in inventory."
+msgstr ""
+
+#: models/product.py:495
+msgid "Can change product amount"
+msgstr ""
+
+#: models/product.py:507 templates/products/index.html:12
+msgid "New Category"
+msgstr ""
+
+#: models/product.py:557
+msgid "minimum amount"
+msgstr ""
+
+#: models/product.py:561
+msgid "reserved amount"
+msgstr ""
+
+#: models/product.py:565
+msgid "stocked amount"
+msgstr ""
+
+#: models/product.py:569
+msgid "ordered amount"
+msgstr ""
+
+#: models/purchases.py:35
+msgid "The location from which this PO was created"
+msgstr ""
+
+#: models/purchases.py:42
+msgid "reference"
+msgstr ""
+
+#: models/purchases.py:48
+msgid "confirmation"
+msgstr ""
+
+#: models/purchases.py:58
+msgid "supplier"
+msgstr ""
+
+#: models/purchases.py:63 models/shipments.py:65
+msgid "carrier"
+msgstr ""
+
+#: models/purchases.py:68
+msgid "tracking ID"
+msgstr ""
+
+#: models/purchases.py:73
+msgid "delivery Time"
+msgstr ""
+
+#: models/purchases.py:125
+#, python-format
+msgid "Purchase Order %d has already been submitted"
+msgstr ""
+
+#: models/purchases.py:178
+msgid "Submitted orders cannot be deleted"
+msgstr ""
+
+#: models/purchases.py:191 templates/products/receive_item.html:36
+#: templates/purchases/edit_po.html:35 templates/purchases/view_po.html:34
+msgid "Purchase Price"
+msgstr ""
+
+#: models/purchases.py:192
+msgid "Purchase price without taxes"
+msgstr ""
+
+#: models/purchases.py:198
+msgid "Purchase Order"
+msgstr ""
+
+#: models/purchases.py:210
+msgid "arrived"
+msgstr ""
+
+#: models/purchases.py:248
+msgid "Product has already been received"
+msgstr ""
+
+#: models/purchases.py:291
+#, python-format
+msgid "Cannot receive item %(prod)s (%(ref)s)"
+msgstr ""
+
+#: models/purchases.py:318 views/purchases.py:215
+#, python-format
+msgid "Purchase Order %d created"
+msgstr ""
+
+#: models/queue.py:26 templates/admin/queues/index.html:5
+msgid "New Queue"
+msgstr ""
+
+#: models/queue.py:33
+msgid ""
+"Orders with devices matching these keywords will be automatically assigned "
+"to this queue"
+msgstr ""
+
+#: models/queue.py:38
+msgid "locations"
+msgstr ""
+
+#: models/queue.py:39
+msgid "Pick the locations you want this queue to appear in."
+msgstr ""
+
+#: models/queue.py:52
+msgid "High"
+msgstr ""
+
+#: models/queue.py:53
+msgid "Normal"
+msgstr ""
+
+#: models/queue.py:54
+msgid "Low"
+msgstr ""
+
+#: models/queue.py:68
+msgid "Order Created"
+msgstr ""
+
+#: models/queue.py:69
+msgid "Order has ben placed to a queue"
+msgstr ""
+
+#: models/queue.py:77
+msgid "Order Assigned"
+msgstr ""
+
+#: models/queue.py:78
+msgid "Order has ben assigned to a user"
+msgstr ""
+
+#: models/queue.py:86
+msgid "Products Ordered"
+msgstr ""
+
+#: models/queue.py:87
+msgid "Purchase Order for this Service Order has been submitted"
+msgstr ""
+
+#: models/queue.py:94
+msgid "Products Received"
+msgstr ""
+
+#: models/queue.py:95
+msgid "Products have been received"
+msgstr ""
+
+#: models/queue.py:102
+msgid "Repair Completed"
+msgstr ""
+
+#: models/queue.py:103
+msgid "GSX repair completed"
+msgstr ""
+
+#: models/queue.py:111
+msgid "Order Dispatched"
+msgstr ""
+
+#: models/queue.py:119
+msgid "Order Closed"
+msgstr ""
+
+#: models/queue.py:127
+msgid "GSX queries of an order in this queue will be made using this Sold-To"
+msgstr ""
+
+#: models/queue.py:134
+msgid "order template"
+msgstr ""
+
+#: models/queue.py:135
+msgid "HTML template for Service Order/Work Confirmation"
+msgstr ""
+
+#: models/queue.py:141
+msgid "quote template"
+msgstr ""
+
+#: models/queue.py:142
+msgid "HTML template for cost estimate"
+msgstr ""
+
+#: models/queue.py:148
+msgid "receipt template"
+msgstr ""
+
+#: models/queue.py:149
+msgid "HTML template for Sales Order Receipt"
+msgstr ""
+
+#: models/queue.py:155
+msgid "dispatch template"
+msgstr ""
+
+#: models/queue.py:156
+msgid "HTML template for dispatched order"
+msgstr ""
+
+#: models/queue.py:175 templates/admin/index.html:21
+#: templates/stats/index.html:10 templates/stats/locations.html:7
+#: templates/stats/queues.html:7 templates/stats/sales.html:7
+#: templates/stats/statuses.html:7 views/admin.py:577
+msgid "Queues"
+msgstr ""
+
+#: models/queue.py:187
+msgid "Minutes"
+msgstr ""
+
+#: models/queue.py:188
+msgid "Hours"
+msgstr ""
+
+#: models/queue.py:189
+msgid "Days"
+msgstr ""
+
+#: models/queue.py:190
+msgid "Weeks"
+msgstr ""
+
+#: models/queue.py:191
+msgid "Months"
+msgstr ""
+
+#: models/queue.py:196 templates/admin/statuses/index.html:5
+msgid "New Status"
+msgstr ""
+
+#: models/queue.py:206 models/queue.py:249
+msgid "green limit"
+msgstr ""
+
+#: models/queue.py:210 models/queue.py:250
+msgid "yellow limit"
+msgstr ""
+
+#: models/queue.py:215 models/queue.py:253
+msgid "time unit"
+msgstr ""
+
+#: models/queue.py:237 templates/accounts/updates.html:28
+#: templates/admin/index.html:18 templates/admin/queues/form.html:10
+#: templates/stats/index.html:11 templates/stats/locations.html:8
+#: templates/stats/queues.html:8 templates/stats/sales.html:8
+#: templates/stats/statuses.html:8 views/admin.py:261
+msgid "Statuses"
+msgstr ""
+
+#: models/repair.py:33 templates/admin/checklist/index.html:5
+msgid "New Checklist"
+msgstr ""
+
+#: models/repair.py:42
+msgid "enabled"
+msgstr ""
+
+#: models/repair.py:55 templates/admin/index.html:36
+msgid "Checklists"
+msgstr ""
+
+#: models/repair.py:60
+msgid "Task"
+msgstr ""
+
+#: models/repair.py:107
+#, fuzzy
+msgid "Unit Received"
+msgstr "Mottagen av"
+
+#: models/repair.py:120
+msgid "Repair should be reviewed by Apple before confirmation"
+msgstr ""
+
+#: models/repair.py:135
+msgid "Notes are mandatory when requesting review."
+msgstr ""
+
+#: models/repair.py:142
+msgid "Choose files to be sent with the repair creation request"
+msgstr ""
+
+#: models/repair.py:148
+msgid "mark complete"
+msgstr ""
+
+#: models/repair.py:149
+msgid "Requires replacement serial number"
+msgstr ""
+
+#: models/repair.py:155
+msgid "New serial number"
+msgstr "Nya serienummer"
+
+#: models/repair.py:156
+msgid "Serial Number of replacement part"
+msgstr ""
+
+#: models/repair.py:175
+msgid "Unit is eligible for consumer law coverage"
+msgstr ""
+
+#: models/repair.py:223
+msgid "New GSX Repair"
+msgstr ""
+
+#: models/repair.py:278
+msgid "Please add some parts to the repair"
+msgstr ""
+
+#: models/repair.py:281
+msgid "Order has not been assigned to a queue"
+msgstr ""
+
+#: models/repair.py:327 views/gsx.py:193
+#, python-format
+msgid "GSX repair %s created"
+msgstr ""
+
+#: models/repair.py:582
+#, python-format
+msgid "Repair %d"
+msgstr ""
+
+#: models/shipments.py:43 templates/shipments/list_bulk_returns.html:10
+msgid "Tracking ID"
+msgstr ""
+
+#: models/shipments.py:90
+msgid "width"
+msgstr ""
+
+#: models/shipments.py:96
+msgid "height"
+msgstr ""
+
+#: models/shipments.py:102
+msgid "length"
+msgstr ""
+
+#: models/shipments.py:108
+msgid "weight"
+msgstr ""
+
+#: templates/default.html:34 templates/default.html.py:66
+msgid "Homepage"
+msgstr ""
+
+#: templates/default.html:36
+msgid "Inventory"
+msgstr ""
+
+#: templates/default.html:37 templates/customers/view.html:9
+#: templates/devices/index.html:77 templates/devices/search.html:14
+#: templates/search/spotlight.html:16 views/device.py:48
+msgid "Devices"
+msgstr ""
+
+#: templates/default.html:38 templates/customers/index.html:82
+#: templates/customers/list.html:5 templates/customers/search.html:33
+#: templates/customers/view.html:117 templates/search/spotlight.html:13
+#: views/customer.py:27
+msgid "Customers"
+msgstr ""
+
+#: templates/default.html:39 templates/accounts/updates.html:27
+#: templates/customers/view.html:10 templates/notes/list_notes.html:36
+#: views/note.py:54
+msgid "Messages"
+msgstr ""
+
+#: templates/default.html:40 templates/accounts/tabs.html:16
+#: templates/stats/index.html:58 views/stats.py:296
+msgid "Statistics"
+msgstr ""
+
+#: templates/default.html:44 templates/accounts/orders.html:44
+#: templates/accounts/stats.html:19 templates/customers/choose.html:18
+#: templates/customers/find.html:16 templates/customers/find.html.py:31
+#: templates/customers/index.html:44 templates/devices/choose.html:18
+#: templates/devices/find.html:8 templates/devices/find.html.py:21
+#: templates/devices/index.html:20 templates/invoices/index.html:31
+#: templates/notes/find.html:9 templates/notes/find.html.py:25
+#: templates/notes/list_notes.html:29 templates/orders/index.html:36
+#: templates/products/choose.html:18 templates/products/index.html:84
+#: templates/purchases/list_pos.html:37
+#: templates/snippets/filtering_form.html:12 templates/stats/index.html:30
+msgid "Search"
+msgstr ""
+
+#: templates/default.html:67 templates/accounts/calendars.html:36
+#: templates/accounts/calendars.html:54 templates/accounts/tabs.html:13
+#: templates/accounts/view_calendar.html:64 views/account.py:128
+msgid "Calendars"
+msgstr ""
+
+#: templates/default.html:68 templates/accounts/settings.html:11
+#: templates/admin/users/form.html:11
+msgid "Profile"
+msgstr ""
+
+#: templates/default.html:70 templates/admin/index.html:7 views/admin.py:205
+msgid "System Settings"
+msgstr ""
+
+#: templates/default.html:73 templates/accounts/logout.html:15
+#: templates/checkin/index.html:47
+msgid "Log out"
+msgstr "Logga ut"
+
+#: templates/default.html:100
+msgid "No new alerts"
+msgstr ""
+
+#: templates/default.html:105
+msgid "See All Alerts"
+msgstr ""
+
+#: templates/default.html:133
+msgid "No new messages"
+msgstr ""
+
+#: templates/default.html:139
+msgid "Read All Messages"
+msgstr ""
+
+#: templates/default.html:168
+msgid "Terms"
+msgstr ""
+
+#: templates/error.html:12
+#, fuzzy
+msgid "An Error Occurred"
+msgstr "Ett fel uppstod"
+
+#: templates/error.html:27 templates/checkin/error.html:9
+msgid "Oops!"
+msgstr "Hoppsan!"
+
+#: templates/error.html:28 templates/checkin/error.html:10
+msgid "It appears that an error has occurred."
+msgstr "Det verkar som om ett fel har uppstått."
+
+#: templates/error.html:28
+msgid ""
+"I've notified the developers, but I'm sure they would really appreciate if "
+"you could briefly describe what you were doing before this happened."
+msgstr ""
+
+#: templates/error.html:33 templates/orders/dispatch.html:96
+#: templates/products/form.html:74 templates/products/form.html.py:76
+#: templates/products/receive_item.html:47
+msgid "Cancel"
+msgstr ""
+
+#: templates/error.html:34 templates/accounts/register.html:11
+#: templates/checkin/confirmation.html:11 templates/checkin/customer.html:62
+#: templates/notes/edit_escalation.html:13
+#: templates/orders/gsx_repair_form.html:79
+#: templates/products/index_outgoing.html:49
+#: templates/purchases/edit_po.html:87 templates/purchases/order_stock.html:15
+#: templates/shipments/edit_bulk_return.html:96
+#: templates/shipments/list_returns.html:38
+#: templates/shipments/submit_bulk_return.html:13
+msgid "Submit"
+msgstr "Fortsätt"
+
+#: templates/error.html:38
+msgid "Thanks!"
+msgstr "Tack!"
+
+#: templates/error.html:39
+msgid ""
+"Your error report has been submitted. Thanks for helping make Servo better!"
+msgstr ""
+
+#: templates/error.html:40
+#, fuzzy
+msgid "Return Home"
+msgstr "Produkter och tjänster"
+
+#: templates/form_buttons.html:3 templates/accounts/register.html:10
+#: templates/admin/users/form.html:50 templates/checkin/choose_model.html:137
+#: templates/checkin/customer.html:61 templates/checkin/device.html:56
+#: templates/checkin/issue.html:19 templates/checkin/sn.html:14
+#: templates/checkin/status.html:14
+msgid "Back"
+msgstr "Bakåt"
+
+#: templates/form_buttons.html:4 templates/admin/settings.html:110
+#: templates/admin/checklist/form.html:28 templates/admin/fields/form.html:14
+#: templates/admin/gsx/form.html:17 templates/admin/locations/form.html:37
+#: templates/admin/notifications/index.html:36
+#: templates/admin/statuses/form.html:18 templates/admin/tags/form.html:14
+#: templates/admin/templates/form.html:18 templates/admin/users/form.html:51
+#: templates/notes/form.html:101 templates/orders/gsx_repair_form.html:79
+#: templates/products/form.html:78 templates/purchases/edit_po.html:87
+#: templates/shipments/edit_bulk_return.html:96
+msgid "Save"
+msgstr ""
+
+#: templates/modal.html:10 templates/admin/fields/remove.html:7
+#: templates/admin/statuses/remove.html:9 templates/admin/users/remove.html:9
+msgid "This action cannot be undone."
+msgstr ""
+
+#: templates/modal.html:15 templates/orders/parts.html:20
+#: templates/products/get_info.html:58 templates/snippets/modal.html:12
+msgid "Done"
+msgstr "Klar"
+
+#: templates/pagination.html:11
+msgid "Page"
+msgstr ""
+
+#: templates/accounts/calendars.html:9
+msgid "New Event"
+msgstr ""
+
+#: templates/accounts/calendars.html:15 templates/checkin/thanks.html:13
+#: templates/orders/toolbar.html:20
+msgid "Print"
+msgstr "Skriv ut"
+
+#: templates/accounts/calendars.html:16 templates/customers/find.html:75
+#: templates/customers/find.html.py:77 templates/customers/view.html:85
+#: templates/customers/view.html.py:87 templates/devices/find.html:54
+#: templates/devices/list.html:27 templates/devices/view.html:60
+#: templates/generic/index.html:15 templates/orders/customer.html:20
+#: templates/orders/devices.html:119 templates/orders/products.html:67
+#: templates/products/get_info.html:56 templates/products/view.html:119
+#: templates/products/view.html.py:121 templates/purchases/list_pos.html:92
+#: templates/purchases/list_pos.html:94
+msgid "Edit"
+msgstr ""
+
+#: templates/accounts/calendars.html:18 templates/accounts/calendars.html:20
+#: templates/accounts/delete_calendar.html:15
+#: templates/accounts/delete_calendar_event.html:11
+#: templates/admin/checklist/form.html:24
+#: templates/admin/checklist/form.html:26 templates/admin/fields/form.html:10
+#: templates/admin/fields/form.html:12 templates/admin/gsx/form.html:13
+#: templates/admin/gsx/form.html.py:15 templates/admin/gsx/remove.html:11
+#: templates/admin/locations/form.html:33
+#: templates/admin/locations/form.html:35 templates/admin/queues/form.html:29
+#: templates/admin/queues/remove.html:15 templates/admin/statuses/form.html:14
+#: templates/admin/statuses/form.html:16
+#: templates/admin/statuses/remove.html:15 templates/admin/tags/form.html:10
+#: templates/admin/tags/form.html.py:12 templates/admin/templates/form.html:14
+#: templates/admin/templates/form.html:16
+#: templates/admin/users/delete_group.html:9
+#: templates/admin/users/form.html:46 templates/admin/users/form.html.py:48
+#: templates/admin/users/remove.html:15
+#: templates/customers/delete_group.html:15 templates/customers/find.html:81
+#: templates/customers/find.html.py:83 templates/customers/remove.html:5
+#: templates/customers/remove.html.py:15 templates/customers/view.html:77
+#: templates/customers/view.html.py:79 templates/devices/find.html:56
+#: templates/devices/list.html:29 templates/devices/remove.html:15
+#: templates/devices/view.html:52 templates/devices/view.html.py:54
+#: templates/generic/delete.html:15 templates/generic/index.html:16
+#: templates/notes/form.html:99 templates/notes/remove.html:15
+#: templates/notes/view_note.html:34 templates/orders/delete_order.html:15
+#: templates/orders/gsx_repair_form.html:77 templates/orders/notes.html:50
+#: templates/orders/products.html:77 templates/orders/remove_product.html:15
+#: templates/products/delete_category.html:15
+#: templates/products/list_rows.html:44 templates/products/list_rows.html:46
+#: templates/products/remove.html:11 templates/products/view.html:109
+#: templates/products/view.html.py:113 templates/purchases/edit_po.html:37
+#: templates/purchases/list_pos.html:97 templates/purchases/list_pos.html:99
+#: templates/repairs/delete_part.html:10
+#: templates/repairs/delete_repair.html:14
+msgid "Delete"
+msgstr ""
+
+#: templates/accounts/calendars.html:26 templates/customers/find.html:7
+#: templates/customers/find.html.py:9
+msgid "Download"
+msgstr ""
+
+#: templates/accounts/calendars.html:47
+msgid "No calendar selected"
+msgstr ""
+
+#: templates/accounts/delete_calendar.html:9
+msgid "This will also delete all events in this calendar"
+msgstr ""
+
+#: templates/accounts/login.html:18
+msgid "Login"
+msgstr ""
+
+#: templates/accounts/logout.html:5
+#, fuzzy
+msgid "Logging out?"
+msgstr "Logga ut"
+
+#: templates/accounts/logout.html:9
+msgid "This will terminate your Servo session."
+msgstr ""
+
+#: templates/accounts/orders.html:7 templates/checkin/welcome.html:12
+#: templates/notes/view_note.html:33 templates/orders/customer.html:18
+#: templates/orders/index.html:7
+msgid "Create Order"
+msgstr "Skapa ärende"
+
+#: templates/accounts/orders.html:18 templates/devices/search.html:6
+#: templates/orders/index.html:18 templates/orders/parts.html:19
+#: templates/products/index.html:49 templates/products/search.html:5
+#: templates/shipments/index.html:12
+msgid "Filter results"
+msgstr ""
+
+#: templates/accounts/orders.html:43 templates/orders/index.html:35
+msgid "Reset"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:29
+#: templates/accounts/print_calendar.html:79
+msgid "Employee"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:30
+#: templates/accounts/print_calendar.html:41
+#: templates/accounts/view_calendar.html:28
+#: templates/orders/print_confirmation.html:33
+#: templates/orders/print_confirmation.html:162
+#: templates/orders/print_confirmation.html:179
+msgid "Date"
+msgstr "Datum"
+
+#: templates/accounts/print_calendar.html:42
+#: templates/accounts/view_calendar.html:29
+msgid "Started At"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:43
+#: templates/accounts/view_calendar.html:30
+msgid "Finished At"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:44
+#: templates/accounts/view_calendar.html:31
+msgid "Duration"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:58
+#: templates/accounts/view_calendar.html:57
+msgid "No events found"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:63 templates/invoices/index.html:48
+#: templates/notes/form.html:25 templates/orders/dispatch.html:67
+#: templates/orders/dispatch.html.py:83
+#: templates/orders/gsx_repair_form.html:34
+#: templates/orders/list_products.html:20
+#: templates/orders/print_confirmation.html:111
+#: templates/orders/print_confirmation.html:139
+#: templates/orders/products.html:14 templates/purchases/edit_po.html:67
+#: templates/purchases/list_pos.html:59
+msgid "Total"
+msgstr ""
+
+#: templates/accounts/print_calendar.html:75
+msgid "Manager"
+msgstr ""
+
+#: templates/accounts/register.html:6 views/account.py:326
+msgid "Register"
+msgstr ""
+
+#: templates/accounts/settings.html:12 templates/admin/locations/form.html:7
+#: templates/admin/queues/form.html:12 templates/admin/users/form.html:12
+#: templates/products/form.html:15 templates/search/spotlight.html:19
+msgid "GSX"
+msgstr ""
+
+#: templates/accounts/settings.html:39 templates/accounts/tabs.html:19
+#: templates/admin/index.html:9
+msgid "Settings"
+msgstr ""
+
+#: templates/accounts/stats.html:28
+msgid "Runrate"
+msgstr ""
+
+#: templates/accounts/stats.html:29
+msgid ""
+"Shows you how many you've been assigned to during the selected period, "
+"averaged over the time scale."
+msgstr ""
+
+#: templates/accounts/tabs.html:10 templates/accounts/updates.html:26
+#: templates/accounts/updates.html.py:64 views/account.py:401
+#, fuzzy
+msgid "Updates"
+msgstr "Inköpsdatum"
+
+#: templates/accounts/updates.html:7
+msgid "Clear all"
+msgstr ""
+
+#: templates/accounts/updates.html:36
+msgid "Event"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:10 views/stats.py:243
+msgid "Day"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:11 views/stats.py:244
+msgid "Week"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:12 views/stats.py:245
+msgid "Month"
+msgstr ""
+
+#: templates/accounts/view_calendar.html:19
+msgid "Today"
+msgstr ""
+
+#: templates/admin/index.html:46
+msgid "Systen Settings"
+msgstr ""
+
+#: templates/admin/settings.html:11 templates/admin/checklist/form.html:9
+#: templates/admin/gsx/form.html:6 templates/admin/locations/form.html:6
+#: templates/admin/queues/form.html:9 templates/admin/statuses/form.html:6
+#: templates/admin/tags/index.html:31 templates/admin/templates/form.html:6
+#: templates/admin/users/form.html:8 templates/customers/form.html:8
+#: templates/devices/form.html:8 templates/products/form.html:10
+msgid "General"
+msgstr ""
+
+#: templates/admin/settings.html:12 templates/admin/inventory/index.html:8
+#: templates/products/form.html:13
+msgid "Stock"
+msgstr ""
+
+#: templates/admin/settings.html:13
+msgid "Outgoing Mail"
+msgstr ""
+
+#: templates/admin/settings.html:14
+msgid "Incoming Mail"
+msgstr ""
+
+#: templates/admin/settings.html:15
+msgid "Text Messages"
+msgstr ""
+
+#: templates/admin/settings.html:16
+#, fuzzy
+msgid "Check-in"
+msgstr "Kontrollera"
+
+#: templates/admin/settings.html:35
+msgid "Update prices"
+msgstr ""
+
+#: templates/admin/settings.html:37
+msgid "Applies the new settings to existing products after saving"
+msgstr ""
+
+#: templates/admin/settings.html:52 views/admin.py:622
+msgid "Notifications"
+msgstr ""
+
+#: templates/admin/settings.html:67
+msgid "Sending SMS messages will be handled by Servo"
+msgstr ""
+
+#: templates/admin/settings.html:98
+msgid "Shipping Methods"
+msgstr ""
+
+#: templates/admin/settings.html:105
+msgid "Add Method"
+msgstr ""
+
+#: templates/admin/checklist/form.html:10
+msgid "Tasks"
+msgstr ""
+
+#: templates/admin/fields/remove.html:6
+msgid "Are you sure you want to delete this field?"
+msgstr ""
+
+#: templates/admin/gsx/index.html:5
+msgid "New Account"
+msgstr ""
+
+#: templates/admin/gsx/remove.html:5
+msgid "Remove GSX account"
+msgstr ""
+
+#: templates/admin/notifications/index.html:6
+msgid "Reports"
+msgstr ""
+
+#: templates/admin/notifications/index.html:15
+msgid "Daily"
+msgstr ""
+
+#: templates/admin/notifications/index.html:16
+msgid "Weekly"
+msgstr ""
+
+#: templates/admin/notifications/index.html:17
+msgid "Monthly"
+msgstr ""
+
+#: templates/admin/notifications/index.html:22
+msgid "Repairs aging beyond limits"
+msgstr ""
+
+#: templates/admin/notifications/index.html:28
+msgid "Products below stocking limit"
+msgstr ""
+
+#: templates/admin/queues/form.html:11
+msgid "Defaults"
+msgstr ""
+
+#: templates/admin/queues/form.html:28
+msgid "Time limits"
+msgstr ""
+
+#: templates/admin/queues/form.html:46
+#, fuzzy
+msgid "Add Status"
+msgstr "Kontrollera status"
+
+#: templates/admin/queues/remove.html:5
+#, python-format
+msgid "Delete queue \"%(title)s\"?"
+msgstr ""
+
+#: templates/admin/queues/remove.html:9
+msgid "Orders in this queue will not be deleted."
+msgstr ""
+
+#: templates/admin/sites/index.html:6 views/admin.py:648
+msgid "New Site"
+msgstr ""
+
+#: templates/admin/sites/index.html:12
+msgid "Sites"
+msgstr ""
+
+#: templates/admin/sites/index.html:20
+msgid "No site selected"
+msgstr ""
+
+#: templates/admin/statuses/remove.html:5
+#, python-format
+msgid "Delete status \"%(title)s\"?"
+msgstr ""
+
+#: templates/admin/tags/index.html:25
+#, python-format
+msgid "No %(objects)s found."
+msgstr ""
+
+#: templates/admin/users/delete_group.html:4
+msgid "Really delete this group?"
+msgstr ""
+
+#: templates/admin/users/delete_group.html:5
+msgid "Group member accounts will not be deleted."
+msgstr ""
+
+#: templates/admin/users/groups.html:13
+msgid "Members"
+msgstr ""
+
+#: templates/admin/users/groups.html:31
+msgid "No groups defined"
+msgstr ""
+
+#: templates/admin/users/index.html:13 views/admin.py:675
+msgid "Upload Users"
+msgstr ""
+
+#: templates/admin/users/remove.html:5
+#, python-format
+msgid "Delete user %(user)s?"
+msgstr ""
+
+#: templates/admin/users/tabs.html:5
+msgid "Users"
+msgstr ""
+
+#: templates/checkin/choose_model.html:6
+msgid "Choose your device"
+msgstr "Välj din enhet"
+
+#: templates/checkin/choose_model.html:7
+msgid "Please choose your device model"
+msgstr "Välj din enhetsmodell"
+
+#: templates/checkin/choose_model.html:14
+msgid "MacBook Air"
+msgstr ""
+
+#: templates/checkin/choose_model.html:20
+msgid "MacBook"
+msgstr ""
+
+#: templates/checkin/choose_model.html:26
+msgid "MacBook Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:32
+msgid "iMac"
+msgstr ""
+
+#: templates/checkin/choose_model.html:40
+#, fuzzy
+msgid "iPhone"
+msgstr "Telefon"
+
+#: templates/checkin/choose_model.html:46
+msgid "iPad"
+msgstr ""
+
+#: templates/checkin/choose_model.html:52
+msgid "Apple TV"
+msgstr ""
+
+#: templates/checkin/choose_model.html:60
+msgid "Mac Pro"
+msgstr ""
+
+#: templates/checkin/choose_model.html:66
+msgid "Display"
+msgstr ""
+
+#: templates/checkin/choose_model.html:72
+msgid "Mac mini"
+msgstr ""
+
+#: templates/checkin/choose_model.html:78
+#, fuzzy
+msgid "Power Mac"
+msgstr "Nätadapter"
+
+#: templates/checkin/choose_model.html:86
+msgid "iPod Touch"
+msgstr ""
+
+#: templates/checkin/choose_model.html:92
+msgid "iPod nano"
+msgstr ""
+
+#: templates/checkin/choose_model.html:98
+msgid "iPod Shuffle"
+msgstr ""
+
+#: templates/checkin/choose_model.html:104
+msgid "iPod Classic"
+msgstr ""
+
+#: templates/checkin/choose_model.html:112
+msgid "Keyboard or Mouse"
+msgstr "Tangentbord eller mus"
+
+#: templates/checkin/choose_model.html:118 views/checkin.py:283
+msgid "Power Adapter"
+msgstr "Nätadapter"
+
+#: templates/checkin/choose_model.html:124
+msgid "AirPort and Wireless"
+msgstr ""
+
+#: templates/checkin/choose_model.html:130
+msgid "Other Device"
+msgstr "Andra enheten"
+
+#: templates/checkin/customer.html:7
+msgid "Your contact details"
+msgstr "Dina kontaktuppgifter"
+
+#: templates/checkin/customer.html:8
+msgid "Please make sure your contact details are filled out correctly."
+msgstr "Se till att dina kontaktuppgifter är rätt ifyllda."
+
+#: templates/checkin/device.html:8
+msgid "Please provide additional details about your device here."
+msgstr "Ange ytterligare uppgifter om din enhet här."
+
+#: templates/checkin/device.html:8
+msgid ""
+"Providing a username and password helps us diagnose the problem faster, "
+"especially with software-related issues."
+msgstr ""
+"Att tillhandahålla ett användarnamn och lösenord hjälper oss att "
+"diagnostisera problemet snabbare, särskilt med mjukvarurelaterade frågor."
+
+#: templates/checkin/device.html:8
+msgid "For faster service time, please upload your purchase receipt."
+msgstr "Ladda alltid upp kvitto för snabbare hantering."
+
+#: templates/checkin/device.html:20
+msgid "Purchased"
+msgstr "Inköpt"
+
+#: templates/checkin/device.html:24 templates/devices/get_info.html:17
+#: templates/devices/search_gsx_warranty.html:17
+#: templates/devices/summary.html:16 templates/orders/devices.html:23
+#: templates/search/results/gsx_warranty.html:17
+msgid "Configration"
+msgstr "Konfiguration"
+
+#: templates/checkin/device.html:28 templates/devices/get_info.html:23
+#: templates/devices/summary.html:34
+#: templates/search/results/gsx_warranty.html:22
+msgid "Activation Profile"
+msgstr "Aktiveringsprofil"
+
+#: templates/checkin/device.html:30 templates/devices/get_info.html:25
+#: templates/devices/search_gsx_warranty.html:26
+#: templates/devices/summary.html:36 templates/orders/devices.html:33
+#: templates/search/results/gsx_warranty.html:24
+msgid "Unlocked"
+msgstr "Olåst"
+
+#: templates/checkin/device.html:34 templates/customers/form.html:9
+#: templates/devices/form.html:9 templates/devices/get_info.html:37
+#: templates/devices/search_gsx_warranty.html:36
+#: templates/orders/devices.html:45 templates/orders/gsx_repair_form.html:40
+#: templates/products/form.html:14
+msgid "Details"
+msgstr "Detaljer"
+
+#: templates/checkin/device.html:38
+#: templates/devices/search_gsx_warranty.html:40
+msgid "Onsite Coverage"
+msgstr ""
+
+#: templates/checkin/device.html:42
+#: templates/devices/search_gsx_warranty.html:44
+msgid "Contract Coverage"
+msgstr ""
+
+#: templates/checkin/device.html:57 templates/checkin/issue.html:20
+#: templates/checkin/sn.html:16
+msgid "Continue"
+msgstr "Fortsätt"
+
+#: templates/checkin/error.html:4
+msgid "An error occurred"
+msgstr "Ett fel uppstod"
+
+#: templates/checkin/error.html:12
+msgid "Try again"
+msgstr "Försök igen"
+
+#: templates/checkin/issue.html:8
+msgid "Please describe the problem in as much detail as possible."
+msgstr "Beskriv problemet så detaljerat som möjligt."
+
+#: templates/checkin/sn.html:7
+msgid "Please enter your serial number"
+msgstr "Hej,"
+
+#: templates/checkin/sn.html:8
+msgid ""
+"You can find instructions for locating the serial number by <a href=\"http://"
+"support.apple.com/kb/ht1349\" target=\"_blank\">clicking here</a>."
+msgstr ""
+"Skriv in Serienumret på den enhet som du vill skicka in på service och klicka på fortsätt. För instruktioner om hur du hittar ditt serienummer klicka <a "
+"href=\"http://support.apple.com/kb/HT1349?viewlocale=sv_SE\" target=\"_blank"
+"\">här</a>."
+
+#: templates/checkin/sn.html:8
+msgid ""
+"Clicking \"Choose\" will let you pick the type of your device if you don't "
+"know the serial number."
+msgstr ""
+"Klicka på \"Välj\" om du vill välja din enhet om du inte kan serienumret."
+
+#: templates/checkin/sn.html:8
+msgid "If you don't provide a serial number, the service time may be longer."
+msgstr ""
+"Om du inte skriver in ditt serienummer kan servicetiden i värsta fall bli "
+"lite längre."
+
+#: templates/checkin/sn.html:9
+msgid ""
+"When checking in an accessory, please provide the serial number of the "
+"device that the accessory is used with."
+msgstr ""
+"För tillbehör behöver vi serienumret som tillbehöret används till."
+
+#: templates/checkin/sn.html:9
+msgid ""
+"For example the serial number of your iPhone if the headset is broken, or "
+"the serial number of your Mac if you have a problem with the mouse."
+msgstr ""
+"Till exempel för ett par trasiga hörlurar behöver vi serienumret på iphonen."
+"Skulle inte enheten ha garanti men tillbehöret är köpt inom ett år så måste "
+"du ladda upp ett kvitto för att stryka garantin."
+
+#: templates/checkin/sn.html:15 templates/orders/customer.html:12
+#: templates/orders/statuses.html:3
+msgid "Choose"
+msgstr "Välj"
+
+#: templates/checkin/status-show.html:33
+msgid "Refresh"
+msgstr ""
+
+#: templates/checkin/status-show.html:34
+msgid "Return"
+msgstr "Återvända"
+
+#: templates/checkin/status.html:7
+msgid "Please enter your Service Order number"
+msgstr "Ange din Arbetsordernummer"
+
+#: templates/checkin/status.html:9
+msgid ""
+"The Service Order number is an 8-digit code printed on your work confirmation"
+msgstr "Arbetsorder numret finns på ditt servicekvitto"
+
+#: templates/checkin/terms.html:6
+#, python-format
+msgid "%(company)s terms of service"
+msgstr ""
+
+#: templates/checkin/thanks.html:7 views/checkin.py:228
+msgid "Done!"
+msgstr "Färdig"
+
+#: templates/checkin/thanks.html:8
+msgid "Your service order has been submitted."
+msgstr "Din serviceorder har skickats till oss."
+
+#: templates/checkin/thanks.html:8
+msgid "Click <strong>Print</strong> to print the confirmation."
+msgstr ""
+"Klicka på <strong>skriv ut</strong> och bifoga serviceordern med ditt ärende."
+
+#: templates/checkin/thanks.html:8
+#, python-format
+msgid "You will be redirected to the start page in %(seconds)s seconds."
+msgstr "Du omdirigeras till startsidan om %(seconds)s sekunder."
+
+#: templates/checkin/troubleshoot.html:12
+msgid "Yes"
+msgstr "Ja"
+
+#: templates/checkin/troubleshoot.html:13
+msgid "No"
+msgstr "Nej"
+
+#: templates/checkin/welcome.html:7
+#, python-format
+msgid "Welcome to %(location)s service check-in."
+msgstr "Välkommen till %(location)s service check-in sida."
+
+#: templates/checkin/welcome.html:8
+msgid ""
+"From here you can <strong>create</strong> a service order or check the "
+"<strong>status</strong> of an existing order."
+msgstr ""
+"Här kan du skapa ett <strong>nytt ärende</strong> eller kontrollera "
+"<strong>status</strong> på ett befintligt ärende."
+
+#: templates/checkin/welcome.html:8
+msgid "Please choose an option below."
+msgstr "Välj ett alternativ nedan"
+
+#: templates/checkin/welcome.html:11
+msgid "Check Status"
+msgstr "Kontrollera status"
+
+#: templates/customers/choose-list.html:6 templates/customers/find.html:91
+#: templates/customers/search.html:28
+#: templates/search/results/customers.html:24
+msgid "No customers found"
+msgstr ""
+
+#: templates/customers/choose.html:5
+msgid "Search for customer"
+msgstr ""
+
+#: templates/customers/choose.html:11
+msgid "Name, email or phone number"
+msgstr ""
+
+#: templates/customers/delete_group.html:5
+msgid "Delete customer group?"
+msgstr ""
+
+#: templates/customers/delete_group.html:9
+msgid "This action will not delete the customers in this group."
+msgstr ""
+
+#: templates/customers/find.html:15 templates/customers/index.html:43
+#: templates/devices/find.html:7 templates/devices/index.html:19
+#: templates/generic/index.html:6 templates/notes/find.html:8
+#: templates/notes/list_notes.html:28
+msgid "Browse"
+msgstr ""
+
+#: templates/customers/find.html:73 templates/devices/find.html:53
+#: templates/devices/list.html:26 templates/products/get_info.html:55
+msgid "View"
+msgstr ""
+
+#: templates/customers/index.html:11 templates/customers/index.html.py:13
+msgid "New Contact"
+msgstr ""
+
+#: templates/customers/index.html:24 templates/devices/index.html:13
+msgid "Upload"
+msgstr ""
+
+#: templates/customers/index.html:27 templates/customers/index.html.py:30
+#: views/admin.py:412
+msgid "Edit Group"
+msgstr ""
+
+#: templates/customers/index.html:28 templates/customers/index.html.py:31
+msgid "Delete Group"
+msgstr ""
+
+#: templates/customers/index.html:57 templates/devices/specs.html:7
+#: templates/products/index.html:61 templates/products/view.html:11
+#: views/product.py:36 views/product.py:326
+msgid "All"
+msgstr ""
+
+#: templates/customers/index.html:74
+msgid "No customer selected"
+msgstr ""
+
+#: templates/customers/merge.html:12 templates/customers/move.html:12
+#, fuzzy
+msgid "Customer name"
+msgstr "Kundsnamn"
+
+#: templates/customers/merge.html:18 templates/customers/move.html:19
+#: templates/orders/close.html:15 templates/orders/toolbar.html:69
+#: templates/orders/toolbar.html.py:73 templates/products/choose.html:17
+#: templates/snippets/modal.html:11
+msgid "Close"
+msgstr ""
+
+#: templates/customers/move.html:5
+msgid "Move customer"
+msgstr ""
+
+#: templates/customers/move.html:18
+msgid "Move to top"
+msgstr ""
+
+#: templates/customers/remove.html:9
+msgid ""
+"This will also delete all customer's contacts. Customer's orders will not be "
+"deleted"
+msgstr ""
+
+#: templates/customers/upload.html:5
+msgid "Upload customer data"
+msgstr ""
+
+#: templates/customers/view.html:48
+msgid "Contacts"
+msgstr ""
+
+#: templates/customers/view.html:55
+msgid "Use in "
+msgstr ""
+
+#: templates/customers/view.html:57 templates/devices/view.html:22
+msgid "Use in current order"
+msgstr ""
+
+#: templates/customers/view.html:61 templates/devices/find.html:50
+#: templates/devices/list.html:23
+#: templates/devices/search_gsx_warranty.html:58
+#: templates/devices/view.html:25
+#: templates/search/results/gsx_warranty.html:33
+msgid "Create Service Order"
+msgstr ""
+
+#: templates/customers/view.html:69 templates/customers/view.html.py:72
+#, fuzzy
+msgid "Move Customer"
+msgstr "Kundsnamn"
+
+#: templates/customers/view.html:70 templates/customers/view.html.py:73
+msgid "Merge Customer"
+msgstr ""
+
+#: templates/customers/view.html:108 templates/notes/list_notes.html:57
+msgid "No messages found"
+msgstr ""
+
+#: templates/devices/accessories_edit.html:4
+msgid "Manage Accessories"
+msgstr ""
+
+#: templates/devices/accessories_edit.html:15
+#: templates/orders/customer.html:22 templates/orders/customer.html.py:24
+#: templates/orders/devices.html:120 templates/orders/followers.html:23
+#: templates/orders/products.html:77 templates/orders/remove_customer.html:15
+#: templates/orders/remove_device.html:15
+msgid "Remove"
+msgstr ""
+
+#: templates/devices/accessories_edit.html:19
+msgid "No accessories added"
+msgstr ""
+
+#: templates/devices/choose-list.html:10 templates/devices/find.html:62
+#: templates/devices/search_gsx_repairs.html:24
+#: templates/search/results/gsx_notfound.html:3
+msgid "No search results"
+msgstr ""
+
+#: templates/devices/choose.html:5
+msgid "Search for device"
+msgstr ""
+
+#: templates/devices/choose.html:11
+msgid "Serial number or IMEI code"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:4
+msgid "Result"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:6
+msgid "Tool"
+msgstr ""
+
+#: templates/devices/diagnostic_results.html:8
+msgid "Passed modules"
+msgstr ""
+
+#: templates/devices/diagnostics.html:4
+msgid "Diagnostics"
+msgstr ""
+
+#: templates/devices/diagnostics.html:11
+msgid "Fetching diagnostics..."
+msgstr ""
+
+#: templates/devices/find.html:29 templates/devices/search_gsx_repairs.html:7
+#: templates/notes/view_note.html:23 templates/orders/list.html:12
+#: templates/products/view.html:132 templates/products/view.html.py:156
+#: templates/products/view.html:181 templates/purchases/list_pos.html:52
+msgid "Created"
+msgstr ""
+
+#: templates/devices/find.html:47 templates/devices/list.html:19
+#: templates/devices/view.html:20 templates/products/view.html:93
+msgid "Use in"
+msgstr ""
+
+#: templates/devices/find.html:51 templates/devices/list.html:24
+msgid "Show Service Orders"
+msgstr ""
+
+#: templates/devices/form.html:27 templates/products/form.html:53
+msgid "Enter tag"
+msgstr ""
+
+#: templates/devices/get_info.html:12
+#: templates/devices/search_gsx_warranty.html:13
+#: templates/devices/summary.html:11 templates/orders/devices.html:18
+#: templates/search/results/gsx_warranty.html:13
+msgid "Purchase Date"
+msgstr ""
+
+#: templates/devices/index.html:8 templates/devices/index.html.py:11
+msgid "Show Parts"
+msgstr ""
+
+#: templates/devices/index.html:36 templates/orders/print_confirmation.html:64
+msgid "Model"
+msgstr ""
+
+#: templates/devices/index.html:40
+msgid "No Product Line selected"
+msgstr ""
+
+#: templates/devices/index.html:58 templates/devices/list.html:35
+#: templates/search/results/devices.html:8
+msgid "No devices found"
+msgstr ""
+
+#: templates/devices/index.html:68
+msgid "No device selected"
+msgstr ""
+
+#: templates/devices/parts.html:9 templates/shipments/edit_bulk_return.html:56
+#: templates/shipments/list_incoming.html:17
+msgid "Part"
+msgstr ""
+
+#: templates/devices/parts.html:21
+#, python-format
+msgid "Substituted to %(code)s"
+msgstr ""
+
+#: templates/devices/parts.html:27 templates/products/choose-list.html:7
+#: templates/products/list_rows.html:54 templates/products/search.html:13
+#: templates/search/results/products.html:8
+msgid "No products found"
+msgstr ""
+
+#: templates/devices/remove.html:5
+#, python-format
+msgid "Delete device %(sn)s\"?"
+msgstr ""
+
+#: templates/devices/remove.html:9
+msgid "This will also remove it from all service orders."
+msgstr ""
+
+#: templates/devices/search_gsx.html:13 templates/devices/search_gsx.html:15
+#: templates/devices/search_gsx_error.html:10
+#: templates/orders/gsx_repair_form.html:31
+#: templates/repairs/get_details.html:22 templates/search/results/gsx.html:13
+#: templates/search/results/gsx.html:15
+#: templates/shipments/list_bulk_returns.html:12
+msgid "Parts"
+msgstr ""
+
+#: templates/devices/search_gsx.html:18 templates/devices/search_gsx.html:20
+#: templates/devices/search_gsx_error.html:11
+#: templates/search/results/gsx.html:18 templates/search/results/gsx.html:20
+msgid "Repairs"
+msgstr ""
+
+#: templates/devices/search_gsx_parts.html:20
+#: templates/devices/search_gsx_parts.html:22
+msgid "Create Product"
+msgstr ""
+
+#: templates/devices/search_gsx_repairs.html:5
+msgid "Number"
+msgstr ""
+
+#: templates/devices/search_gsx_warranty.html:31
+#: templates/purchases/view_po.html:19
+msgid "Sales Order"
+msgstr ""
+
+#: templates/devices/search_gsx_warranty.html:55
+#: templates/search/results/gsx_warranty.html:35
+msgid "Use in order"
+msgstr ""
+
+#: templates/devices/search_gsx_warranty.html:60
+#: templates/search/results/gsx_warranty.html:39
+#: templates/search/results/gsx_warranty.html:41
+msgid "Download Manual"
+msgstr ""
+
+#: templates/devices/specs.html:8
+msgid "Specs"
+msgstr ""
+
+#: templates/devices/specs.html:10
+msgid "Tagi"
+msgstr ""
+
+#: templates/devices/specs.html:17
+msgid "Luo malli"
+msgstr ""
+
+#: templates/devices/upload_devices.html:5
+msgid "Upload devices"
+msgstr ""
+
+#: templates/devices/view.html:10 templates/devices/view.html.py:12
+msgid "GSX Repairs"
+msgstr ""
+
+#: templates/devices/view.html:33 templates/devices/view.html.py:35
+msgid "Service Manual"
+msgstr ""
+
+#: templates/devices/view.html:44 templates/devices/view.html.py:47
+#: templates/orders/devices.html:93
+msgid "Update Warranty Status"
+msgstr ""
+
+#: templates/devices/view.html:45 templates/devices/view.html.py:48
+msgid "Fetch Diagnostics"
+msgstr ""
+
+#: templates/devices/view.html:73
+msgid "Fetching repairs..."
+msgstr ""
+
+#: templates/generic/index.html:22
+msgid "Create New"
+msgstr ""
+
+#: templates/invoices/index.html:7 templates/invoices/index.html.py:105
+#: templates/products/tabs.html:17 templates/products/view.html:46
+#: views/invoices.py:23
+msgid "Invoices"
+msgstr ""
+
+#: templates/invoices/index.html:8
+msgid "Sales Invoices"
+msgstr ""
+
+#: templates/invoices/index.html:9
+msgid "Apple Invoices"
+msgstr ""
+
+#: templates/invoices/index.html:46
+msgid "Date Created"
+msgstr ""
+
+#: templates/invoices/index.html:49
+msgid "Date Paid"
+msgstr ""
+
+#: templates/invoices/index.html:71
+msgid "Print receipt"
+msgstr ""
+
+#: templates/invoices/index.html:78
+msgid "No invoices found"
+msgstr ""
+
+#: templates/notes/form.html:38
+msgid "Attachments"
+msgstr "Bifogade filer"
+
+#: templates/notes/form.html:39
+msgid "Labels"
+msgstr ""
+
+#: templates/notes/form.html:40
+msgid "Escalation"
+msgstr ""
+
+#: templates/notes/form.html:110
+msgid "Edit Note"
+msgstr ""
+
+#: templates/notes/list_notes.html:11
+msgid "New Message"
+msgstr ""
+
+#: templates/notes/list_notes.html:12
+msgid "New Escalation"
+msgstr ""
+
+#: templates/notes/list_notes.html:37
+msgid "Inbox"
+msgstr ""
+
+#: templates/notes/list_notes.html:38
+msgid "Flagged"
+msgstr ""
+
+#: templates/notes/list_notes.html:39
+msgid "Sent"
+msgstr ""
+
+#: templates/notes/list_notes.html:40
+msgid "Escalations"
+msgstr ""
+
+#: templates/notes/list_notes.html:65
+msgid "No message selected"
+msgstr ""
+
+#: templates/notes/messages.html:5 templates/orders/notes.html:48
+msgid "Message Log"
+msgstr ""
+
+#: templates/notes/messages.html:18
+msgid "No messages to display"
+msgstr ""
+
+#: templates/notes/remove.html:5
+msgid "Delete this note?"
+msgstr ""
+
+#: templates/notes/remove.html:9
+msgid "This will also delete any replies to this note."
+msgstr ""
+
+#: templates/notes/search-results.html:21
+msgid "No notes found"
+msgstr ""
+
+#: templates/notes/search.html:7
+msgid "Create Note"
+msgstr ""
+
+#: templates/notes/view_note.html:24
+msgid "Subject"
+msgstr ""
+
+#: templates/notes/view_note.html:35 templates/orders/notes.html:21
+msgid "Reply"
+msgstr ""
+
+#: templates/orders/close.html:5
+msgid "Close order"
+msgstr ""
+
+#: templates/orders/close.html:9
+msgid "This order will no longer be editable."
+msgstr ""
+
+#: templates/orders/close_repair.html:5
+msgid "Complete the GSX repair?"
+msgstr ""
+
+#: templates/orders/close_repair.html:9
+msgid "The GSX repair will no longer be editable."
+msgstr ""
+
+#: templates/orders/close_repair.html:15
+msgid "Complete"
+msgstr ""
+
+#: templates/orders/customer.html:14 templates/orders/customer.html.py:16
+msgid "Add contact"
+msgstr ""
+
+#: templates/orders/delete_order.html:5
+#, python-format
+msgid "Really delete order %(id)s? "
+msgstr ""
+
+#: templates/orders/delete_order.html:9
+msgid ""
+"This will also delete all the order's notes. This action cannot be undone."
+msgstr ""
+
+#: templates/orders/devices.html:61 templates/orders/devices.html.py:63
+msgid "View Parts"
+msgstr ""
+
+#: templates/orders/devices.html:63
+msgid "Assign order to a queue first"
+msgstr ""
+
+#: templates/orders/devices.html:65
+msgid "View History"
+msgstr ""
+
+#: templates/orders/devices.html:67 templates/orders/devices.html.py:69
+msgid "View Accessories"
+msgstr ""
+
+#: templates/orders/devices.html:78 templates/orders/devices.html.py:80
+msgid "Create Carry-In Repair"
+msgstr ""
+
+#: templates/orders/devices.html:80
+msgid "Device has open GSX repairs"
+msgstr ""
+
+#: templates/orders/devices.html:83 templates/orders/devices.html.py:85
+msgid "Create Onsite Repair"
+msgstr ""
+
+#: templates/orders/devices.html:88 templates/orders/devices.html.py:90
+msgid "Create Whole-Unit Exchange"
+msgstr ""
+
+#: templates/orders/devices.html:94
+msgid "Get Diagnostics"
+msgstr ""
+
+#: templates/orders/devices.html:97 templates/orders/devices.html.py:99
+#: templates/orders/toolbar.html:91
+msgid "Show Barcode"
+msgstr ""
+
+#: templates/orders/devices.html:102
+msgid "Show Service Manual"
+msgstr ""
+
+#: templates/orders/devices.html:105
+msgid "Show Exploded View"
+msgstr ""
+
+#: templates/orders/devices.html:112 templates/orders/devices.html.py:114
+msgid "Report"
+msgstr ""
+
+#: templates/orders/devices.html:131
+msgid "Order doesn't contain any devices"
+msgstr ""
+
+#: templates/orders/devices.html:136 templates/orders/toolbar.html:14
+msgid "Add Device"
+msgstr ""
+
+#: templates/orders/dispatch.html:40
+#, fuzzy
+msgid "Payment"
+msgstr "Betalningssätt"
+
+#: templates/orders/dispatch.html:58 templates/products/index.html:100
+#: templates/products/search.html:18 templates/products/tabs.html:8
+#: templates/products/view.html:25 templates/purchases/edit_po.html:21
+#: templates/purchases/list_pos.html:58 views/product.py:31
+msgid "Products"
+msgstr ""
+
+#: templates/orders/dispatch.html:64
+#: templates/orders/print_confirmation.html:108
+#: templates/orders/print_dispatch.html:16
+msgid "Qty"
+msgstr "ant."
+
+#: templates/orders/dispatch.html:65
+#: templates/orders/print_confirmation.html:110
+#: templates/orders/products.html:13 templates/products/view.html:131
+#: templates/products/view.html.py:157 templates/products/view.html:182
+msgid "Price"
+msgstr "Pris"
+
+#: templates/orders/dispatch.html:66
+msgid "Price w/ tax"
+msgstr ""
+
+#: templates/orders/dispatch.html:97 templates/orders/dispatch.html.py:106
+#: templates/orders/toolbar.html:64 templates/orders/toolbar.html.py:66
+#: templates/purchases/edit_po.html:22
+msgid "Dispatch"
+msgstr ""
+
+#: templates/orders/edit.html:23
+msgid "Checkin Location"
+msgstr ""
+
+#: templates/orders/edit.html:43
+msgid "Checkout Location"
+msgstr ""
+
+#: templates/orders/edit.html:69
+msgid "Select Customer"
+msgstr ""
+
+#: templates/orders/edit.html:78
+msgid "Place"
+msgstr ""
+
+#: templates/orders/edit.html:79
+msgid "Enter place"
+msgstr ""
+
+#: templates/orders/edit.html:86
+msgid "No place"
+msgstr ""
+
+#: templates/orders/edit.html:92
+msgid "Enter label"
+msgstr ""
+
+#: templates/orders/edit.html:97
+msgid "No labels"
+msgstr ""
+
+#: templates/orders/followers.html:5
+msgid "Followers"
+msgstr ""
+
+#: templates/orders/followers.html:6
+msgid "Enter name"
+msgstr ""
+
+#: templates/orders/followers.html:19 templates/orders/followers.html.py:21
+msgid "Make primary"
+msgstr ""
+
+#: templates/orders/followers.html:27
+msgid "No followers"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:32
+msgid "Checking warranty"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:39
+msgid "Repair"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:42
+msgid "Components"
+msgstr ""
+
+#: templates/orders/gsx_repair_form.html:73
+#: templates/purchases/edit_po.html:85
+#: templates/shipments/edit_bulk_return.html:94
+msgid "Confirm"
+msgstr ""
+
+#: templates/orders/list.html:22
+msgid "No customer"
+msgstr ""
+
+#: templates/orders/list.html:22
+#, fuzzy
+msgid "No description"
+msgstr "Enhet"
+
+#: templates/orders/list.html:29
+msgid "Nobody"
+msgstr ""
+
+#: templates/orders/list.html:31
+msgid "No status"
+msgstr ""
+
+#: templates/orders/list.html:36 templates/orders/search.html:6
+#: templates/search/results/orders.html:8
+msgid "No orders found"
+msgstr ""
+
+#: templates/orders/list_products.html:12
+msgid "Click to select EEE code"
+msgstr ""
+
+#: templates/orders/list_products.html:16
+msgid "No products added"
+msgstr ""
+
+#: templates/orders/notes.html:8
+msgid "Order doesn't contain any notes."
+msgstr ""
+
+#: templates/orders/notes.html:40
+msgid "Mark"
+msgstr ""
+
+#: templates/orders/notes.html:47 templates/orders/repair.html:10
+msgid "Copy"
+msgstr ""
+
+#: templates/orders/notes.html:65
+msgid "Note has been sent"
+msgstr ""
+
+#: templates/orders/notes.html:83 templates/orders/toolbar.html:6
+#: templates/orders/toolbar.html.py:8
+msgid "Add Note"
+msgstr ""
+
+#: templates/orders/parts.html:15
+#, fuzzy
+msgid "Parts and Services"
+msgstr "Produkter och tjänster"
+
+#: templates/orders/parts.html:28
+msgid "Fetching parts..."
+msgstr ""
+
+#: templates/orders/parts.html:35 templates/stats/sales.html:21
+msgid "Service Parts"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:26
+msgid "Repair Number"
+msgstr "Arbetsorder"
+
+#: templates/orders/print_confirmation.html:37
+msgid "Work Confirmation"
+msgstr "Arbetsorder"
+
+#: templates/orders/print_confirmation.html:44
+msgid "Customer Information"
+msgstr "Kundinformation"
+
+#: templates/orders/print_confirmation.html:45
+msgid "Product Information"
+msgstr "Produktinformation"
+
+#: templates/orders/print_confirmation.html:53
+#: templates/orders/print_receipt.html:11
+msgid "Daytime Phone"
+msgstr "Telefon"
+
+#: templates/orders/print_confirmation.html:66
+msgid "Estimated Date of Purchase"
+msgstr "Beräknad Inköpsdatum"
+
+#: templates/orders/print_confirmation.html:69
+#, fuzzy
+msgid "Activation Policy"
+msgstr "Aktiveringsprofil"
+
+#: templates/orders/print_confirmation.html:89
+msgid "Work Description"
+msgstr "Arbetsbeskrivning"
+
+#: templates/orders/print_confirmation.html:101
+#: templates/orders/print_dispatch.html:9
+msgid "Products and Services"
+msgstr "Produkter och tjänster"
+
+#: templates/orders/print_confirmation.html:107
+#: templates/orders/print_dispatch.html:15
+#, python-format
+msgid "VAT %%"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:109
+#, python-format
+msgid "0%% VAT"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:119
+#: templates/orders/print_dispatch.html:24
+#: templates/orders/print_receipt.html:23
+msgid "SN"
+msgstr ""
+
+#: templates/orders/print_confirmation.html:151
+msgid "Customer Pickup"
+msgstr "Godkänner servicevillkor"
+
+#: templates/orders/print_confirmation.html:160
+msgid "Customer Name"
+msgstr "Kundsnamn"
+
+#: templates/orders/print_confirmation.html:161
+#: templates/orders/print_confirmation.html:178
+msgid "Signature"
+msgstr "Signatur"
+
+#: templates/orders/print_confirmation.html:168
+msgid "Received By"
+msgstr "Mottagen av"
+
+#: templates/orders/print_dispatch.html:5
+#, python-format
+msgid "Dispatch #%(code)s"
+msgstr ""
+
+#: templates/orders/print_dispatch.html:25
+msgid "mon."
+msgstr ""
+
+#: templates/orders/print_receipt.html:6
+#, python-format
+msgid "Receipt %(number)s"
+msgstr "Kvitto %(number)s"
+
+#: templates/orders/print_receipt.html:40
+msgid "Date of invoice"
+msgstr "Fakturadatum"
+
+#: templates/orders/print_receipt.html:41
+msgid "Payment method"
+msgstr "Betalningssätt"
+
+#: templates/orders/print_receipt.html:42
+msgid "Sales Person"
+msgstr "Säljare"
+
+#: templates/orders/products.html:11 templates/purchases/edit_po.html:34
+#: templates/purchases/view_po.html:33
+msgid "Amount"
+msgstr "Mängd"
+
+#: templates/orders/products.html:12
+msgid "Price Category"
+msgstr "Priskategorin"
+
+#: templates/orders/products.html:43 templates/orders/products.html.py:45
+msgid "Add to Repair"
+msgstr ""
+
+#: templates/orders/products.html:72 templates/orders/products.html.py:74
+msgid "Create Device"
+msgstr ""
+
+#: templates/orders/products.html:86
+msgid "Order doesn't contain any products."
+msgstr ""
+
+#: templates/orders/products.html:91 templates/orders/toolbar.html:15
+#: templates/purchases/edit_po.html:6
+msgid "Add Product"
+msgstr ""
+
+#: templates/orders/products.html:96
+#, fuzzy
+msgid "Order Total"
+msgstr "Arbet"
+
+#: templates/orders/remove_customer.html:5
+msgid "Remove this customer from the order?"
+msgstr ""
+
+#: templates/orders/remove_customer.html:9
+msgid "This will not delete the customer from the database."
+msgstr ""
+
+#: templates/orders/remove_device.html:5
+#, python-format
+msgid "Remove %(dev)s?"
+msgstr ""
+
+#: templates/orders/remove_device.html:9
+msgid "This will not delete the device from the database."
+msgstr ""
+
+#: templates/orders/remove_product.html:5
+msgid "Really remove product from order?"
+msgstr ""
+
+#: templates/orders/remove_product.html:9
+msgid "Product will not be deleted from the warehouse."
+msgstr ""
+
+#: templates/orders/repair.html:6 templates/orders/repair.html.py:8
+msgid "Complete Repair"
+msgstr ""
+
+#: templates/orders/repair.html:16
+msgid "Symptom"
+msgstr ""
+
+#: templates/orders/repair.html:18
+msgid "Diagnosis"
+msgstr ""
+
+#: templates/orders/repair.html:20
+msgid "Request Review"
+msgstr ""
+
+#: templates/orders/reserve_products.html:5
+#, python-format
+msgid "Reserve all products in order %(id)s?"
+msgstr ""
+
+#: templates/orders/reserve_products.html:11
+msgid "Reserve"
+msgstr ""
+
+#: templates/orders/tabs.html:8
+msgid "Events"
+msgstr ""
+
+#: templates/orders/toolbar.html:16
+msgid "Choose Customer"
+msgstr ""
+
+#: templates/orders/toolbar.html:25
+msgid "Cost Estimate"
+msgstr ""
+
+#: templates/orders/toolbar.html:27
+#, fuzzy
+msgid "Receipt"
+msgstr "Mottagen av"
+
+#: templates/orders/toolbar.html:28
+msgctxt "noun"
+msgid "Dispatch"
+msgstr ""
+
+#: templates/orders/toolbar.html:42 templates/orders/toolbar.html.py:44
+msgid "Remove Queue"
+msgstr ""
+
+#: templates/orders/toolbar.html:59
+msgid "Remove Status"
+msgstr ""
+
+#: templates/orders/toolbar.html:71
+msgid "Reopen"
+msgstr ""
+
+#: templates/orders/toolbar.html:82
+msgid "Unfollow Order"
+msgstr ""
+
+#: templates/orders/toolbar.html:84
+#, fuzzy
+msgid "Follow Order"
+msgstr "Arbet"
+
+#: templates/orders/toolbar.html:87 templates/orders/toolbar.html.py:89
+#, fuzzy
+msgid "Copy Order"
+msgstr "Arbet"
+
+#: templates/orders/toolbar.html:94
+msgid "Priority"
+msgstr ""
+
+#: templates/orders/toolbar.html:103 templates/orders/toolbar.html.py:106
+msgid "Reserve Products"
+msgstr ""
+
+#: templates/orders/toolbar.html:104 templates/orders/toolbar.html.py:107
+msgid "Order Products"
+msgstr ""
+
+#: templates/orders/toolbar.html:111 templates/orders/toolbar.html.py:113
+#, fuzzy
+msgid "Delete Order"
+msgstr "Skapa ärende"
+
+#: templates/products/category_form.html:5
+msgid "Edit Product Group"
+msgstr ""
+
+#: templates/products/choose.html:5
+msgid "Search for a product"
+msgstr ""
+
+#: templates/products/choose.html:11
+msgid "Code, title or category"
+msgstr ""
+
+#: templates/products/delete_category.html:5
+msgid "Delete product category?"
+msgstr ""
+
+#: templates/products/delete_category.html:9
+msgid "This action will not delete the products in this category."
+msgstr ""
+
+#: templates/products/form.html:42
+msgid "Add"
+msgstr ""
+
+#: templates/products/get_info.html:6
+#, fuzzy
+msgid "Product Info"
+msgstr "Produktinformation"
+
+#: templates/products/get_info.html:26 templates/products/view.html:67
+msgid "Component Group"
+msgstr ""
+
+#: templates/products/get_info.html:31
+msgid "EEE Code"
+msgstr ""
+
+#: templates/products/get_info.html:40 templates/products/view.html:82
+msgid "Stocked"
+msgstr ""
+
+#: templates/products/get_info.html:42 templates/products/receive_item.html:15
+#: templates/products/view.html:84 templates/purchases/list_pos.html:53
+#: templates/shipments/list_incoming.html:20
+msgid "Ordered"
+msgstr ""
+
+#: templates/products/get_info.html:44 templates/products/view.html:86
+msgid "Reserved"
+msgstr ""
+
+#: templates/products/get_info.html:49
+#, fuzzy
+msgid "Product not in inventory"
+msgstr "Produktinformation"
+
+#: templates/products/index.html:15 templates/products/index.html.py:18
+msgid "New Sales Order"
+msgstr ""
+
+#: templates/products/index.html:16 templates/products/index.html.py:19
+msgid "New Purchase Order"
+msgstr ""
+
+#: templates/products/index.html:30 templates/products/index.html.py:33
+msgid "Edit Category"
+msgstr ""
+
+#: templates/products/index.html:31 templates/products/index.html.py:34
+msgid "Delete Category"
+msgstr ""
+
+#: templates/products/index.html:37
+msgid "Download Products"
+msgstr ""
+
+#: templates/products/index.html:38
+msgid "Upload Products"
+msgstr ""
+
+#: templates/products/index.html:39
+msgid "Upload Parts Database"
+msgstr ""
+
+#: templates/products/index_outgoing.html:10
+#: templates/products/index_outgoing.html:55
+msgid "Outgoing"
+msgstr ""
+
+#: templates/products/index_outgoing.html:11
+msgid "Shipped"
+msgstr ""
+
+#: templates/products/index_outgoing.html:23
+#: templates/purchases/edit_po.html:31
+#: templates/shipments/list_returns.html:12
+msgid "Code"
+msgstr ""
+
+#: templates/products/index_outgoing.html:26
+#: templates/shipments/edit_bulk_return.html:58
+#: templates/shipments/list_returns.html:15
+msgid "Return Order"
+msgstr ""
+
+#: templates/products/index_outgoing.html:27
+#: templates/shipments/list_returns.html:16
+msgid "Register For Return"
+msgstr ""
+
+#: templates/products/index_outgoing.html:44
+#: templates/shipments/list_returns.html:33
+msgid "No parts pending return"
+msgstr ""
+
+#: templates/products/list.html:10
+msgid "In Stock"
+msgstr ""
+
+#: templates/products/list_rows.html:31
+msgid "Use in Service Order"
+msgstr ""
+
+#: templates/products/list_rows.html:35 templates/products/view.html:96
+msgid "Create Sales Order"
+msgstr ""
+
+#: templates/products/list_rows.html:36
+msgid "Create Purchase Order"
+msgstr ""
+
+#: templates/products/list_rows.html:39
+msgid "Use in Purchase Order"
+msgstr ""
+
+#: templates/products/receive_item.html:21
+msgid "Stocking Order"
+msgstr ""
+
+#: templates/products/receive_item.html:49
+#: templates/shipments/list_incoming.html:52
+msgid "Receive"
+msgstr ""
+
+#: templates/products/remove.html:5
+#, python-format
+msgid "Really delete product \"%(code)s\"?"
+msgstr ""
+
+#: templates/products/tabs.html:11
+msgid "Shipments"
+msgstr ""
+
+#: templates/products/tabs.html:14 templates/purchases/list_pos.html:131
+#: templates/purchases/view_po.html:55 views/purchases.py:28
+msgid "Purchase Orders"
+msgstr ""
+
+#: templates/products/upload_gsx_parts.html:5
+msgid "Upload GSX Parts"
+msgstr ""
+
+#: templates/products/view.html:44 templates/stats/index.html:12
+#: templates/stats/locations.html:9 templates/stats/queues.html:9
+#: templates/stats/sales.html:9 templates/stats/sales.html.py:13
+#: templates/stats/statuses.html:9
+msgid "Sales"
+msgstr ""
+
+#: templates/products/view.html:45 templates/stats/sales.html:17
+msgid "Purchases"
+msgstr ""
+
+#: templates/products/view.html:104 templates/products/view.html.py:106
+#: templates/products/view.html:111
+#, fuzzy
+msgid "Update Price"
+msgstr "Inköpsdatum"
+
+#: templates/products/view.html:133
+msgid "Dispatched"
+msgstr ""
+
+#: templates/products/view.html:147
+msgid "No Sales Orders"
+msgstr ""
+
+#: templates/products/view.html:171 templates/purchases/list_pos.html:106
+msgid "No Purchase Orders"
+msgstr ""
+
+#: templates/products/view.html:195
+msgid "No invoices"
+msgstr ""
+
+#: templates/purchases/edit_po.html:61
+msgid "Order does not contain any products"
+msgstr ""
+
+#: templates/purchases/list_pos.html:7
+msgid "New"
+msgstr ""
+
+#: templates/purchases/list_pos.html:86 templates/purchases/list_pos.html:88
+msgid "Create GSX Stocking Order"
+msgstr ""
+
+#: templates/purchases/order_stock.html:5
+msgid "Submit this stocking order?"
+msgstr ""
+
+#: templates/purchases/order_stock.html:9
+msgid "Products will be ordered from GSX."
+msgstr ""
+
+#: templates/purchases/view_po.html:15
+msgid "Created By"
+msgstr ""
+
+#: templates/purchases/view_po.html:17
+msgid "Created At"
+msgstr ""
+
+#: templates/purchases/view_po.html:35
+#, fuzzy
+msgid "Received At"
+msgstr "Mottagen av"
+
+#: templates/repairs/add_part.html:5
+#, python-format
+msgid "Add part to repair %(repair)s?"
+msgstr ""
+
+#: templates/repairs/delete_part.html:5
+msgid "Remove this part from the GSX repair?"
+msgstr ""
+
+#: templates/repairs/delete_repair.html:5
+msgid "Delete this GSX repair?"
+msgstr ""
+
+#: templates/repairs/delete_repair.html:9
+msgid "Only repairs that have not been submitted can be deleted."
+msgstr ""
+
+#: templates/repairs/get_details.html:12
+#: templates/search/results/gsx_repair_details.html:8
+msgid "CS Code"
+msgstr ""
+
+#: templates/repairs/get_details.html:14
+#: templates/search/results/gsx_repair_details.html:10
+msgid "Tracking Number"
+msgstr ""
+
+#: templates/repairs/get_details.html:18
+#: templates/search/results/gsx_repair_details.html:14
+msgid "Warranty Coverage"
+msgstr ""
+
+#: templates/repairs/part_menu.html:4 templates/repairs/part_menu.html.py:7
+msgid "Print Return Label"
+msgstr ""
+
+#: templates/repairs/part_menu.html:7
+#, fuzzy
+msgid "Part has no return order number"
+msgstr "Produkter och tjänster"
+
+#: templates/repairs/part_menu.html:11 templates/repairs/part_menu.html:13
+msgid "Update Serial Numbers"
+msgstr ""
+
+#: templates/repairs/part_menu.html:17 templates/repairs/part_menu.html:27
+msgid "Return DOA"
+msgstr ""
+
+#: templates/repairs/part_menu.html:18 templates/repairs/part_menu.html:28
+#: views/shipments.py:326
+msgid "Return Good Part"
+msgstr ""
+
+#: templates/repairs/part_menu.html:19 templates/repairs/part_menu.html:29
+#: views/shipments.py:330
+msgid "Convert to Stock"
+msgstr ""
+
+#: templates/repairs/part_menu.html:22 templates/repairs/part_menu.html:24
+#: templates/repairs/part_menu.html:31
+msgid "Remove from Repair"
+msgstr ""
+
+#: templates/search/spotlight.html:11
+msgid "Results"
+msgstr ""
+
+#: templates/search/spotlight.html:25
+msgid "Products and Parts"
+msgstr ""
+
+#: templates/search/spotlight.html:28
+msgid "Articles"
+msgstr ""
+
+#: templates/search/spotlight.html:36
+msgid "Home"
+msgstr ""
+
+#: templates/shipments/add_to_return-results.html:6
+msgid "No parts found"
+msgstr ""
+
+#: templates/shipments/add_to_return.html:5
+msgid "Add part to return"
+msgstr ""
+
+#: templates/shipments/add_to_return.html:11
+#, fuzzy
+msgid "Return order number"
+msgstr "Produkter och tjänster"
+
+#: templates/shipments/edit_bulk_return.html:6
+msgid "Add Part"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:9
+#: templates/shipments/edit_bulk_return.html:11
+msgid "Verify"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:28
+#: templates/shipments/view_bulk_return.html:6
+msgid "Carrier"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:34
+#: templates/shipments/view_bulk_return.html:8
+msgid "Tracking"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:40
+#: templates/shipments/view_bulk_return.html:10
+msgid "Dimensions"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:46
+#: templates/shipments/view_bulk_return.html:12
+msgid "Weight"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:59
+msgid "Overpack"
+msgstr ""
+
+#: templates/shipments/edit_bulk_return.html:85
+#: templates/shipments/view_bulk_return.html:28
+msgid "No parts registered for this shipment"
+msgstr ""
+
+#: templates/shipments/index.html:26 templates/shipments/list_incoming.html:60
+msgid "Incoming"
+msgstr ""
+
+#: templates/shipments/index.html:29 templates/shipments/list_returns.html:43
+msgid "Parts Pending Return"
+msgstr ""
+
+#: templates/shipments/index.html:31
+#: templates/shipments/list_bulk_returns.html:36
+msgid "Browse Returns"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:9
+msgid "ID"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:11
+msgid "Tracking URL"
+msgstr ""
+
+#: templates/shipments/list_bulk_returns.html:13
+msgid "Returned"
+msgstr ""
+
+#: templates/shipments/list_incoming.html:46
+msgid "No incoming products"
+msgstr ""
+
+#: templates/shipments/submit_bulk_return.html:4
+msgid "Submit the bulk return?"
+msgstr ""
+
+#: templates/shipments/view_bulk_return.html:15
+msgid "Open Packing List"
+msgstr ""
+
+#: templates/snippets/error_modal.html:5
+msgid "An error occured..."
+msgstr ""
+
+#: templates/stats/index.html:8 templates/stats/locations.html:5
+#: templates/stats/queues.html:5 templates/stats/sales.html:5
+#: templates/stats/statuses.html:5
+msgid "Technicians"
+msgstr ""
+
+#: templates/stats/index.html:40
+#, fuzzy
+msgid "Orders Assigned"
+msgstr "Arbet"
+
+#: templates/stats/index.html:41
+msgid ""
+"Shows how many new orders have been assigned to each technician over the "
+"given time period."
+msgstr ""
+
+#: templates/stats/index.html:44 templates/stats/locations.html:13
+#: templates/stats/queues.html:13
+msgid "Orders Created"
+msgstr ""
+
+#: templates/stats/index.html:45
+msgid "This graph shows how many orders are checked in by each user."
+msgstr ""
+
+#: templates/stats/index.html:48 templates/stats/locations.html:29
+#: templates/stats/queues.html:25
+#, fuzzy
+msgid "Work Distribution"
+msgstr "Arbetsbeskrivning"
+
+#: templates/stats/index.html:49
+msgid ""
+"Shows you how the total number of service orders is distributed across the "
+"technicians at this location."
+msgstr ""
+
+#: templates/stats/locations.html:14
+msgid "Shows you how many orders are created at each location."
+msgstr ""
+
+#: templates/stats/locations.html:17 templates/stats/queues.html:17
+#, fuzzy
+msgid "Orders Closed"
+msgstr "Arbet"
+
+#: templates/stats/locations.html:18
+msgid "Shows you how many orders have been closed at each location."
+msgstr ""
+
+#: templates/stats/locations.html:21 templates/stats/queues.html:21
+msgid "Average Turnaround"
+msgstr ""
+
+#: templates/stats/locations.html:22
+msgid "Shows how many hours it takes to complete an order at each location."
+msgstr ""
+
+#: templates/stats/locations.html:25
+msgid "Average Runrate"
+msgstr ""
+
+#: templates/stats/locations.html:26
+msgid "Shows you how many orders people are working on at each location."
+msgstr ""
+
+#: templates/stats/locations.html:30
+msgid ""
+"This shows you how your overall work load is distributed across your service "
+"locations."
+msgstr ""
+
+#: templates/stats/queues.html:14
+msgid ""
+"This is your total number of orders per queue in the specified time period"
+msgstr ""
+
+#: templates/stats/queues.html:18
+msgid "Shows you how many orders have been closed in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:22
+msgid "Shows how many hours it takes to complete an order in each queue."
+msgstr ""
+
+#: templates/stats/queues.html:26
+msgid ""
+"This shows your total ratio of orders over the time period distributed over "
+"each queue."
+msgstr ""
+
+#: templates/stats/sales.html:14
+msgid "Shows you invoice totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:18
+msgid ""
+"Shows you Purchase Order totals per queue within the selected time period."
+msgstr ""
+
+#: templates/stats/sales.html:22
+msgid "Shows you how many parts have been ordered for each labour tier."
+msgstr ""
+
+#: templates/stats/statuses.html:13
+msgid "Orders per location"
+msgstr ""
+
+#: templates/stats/statuses.html:14
+msgid ""
+"Shows the number of orders with a particular status at the selected location "
+"that have been assigned to a technician over the specified time period."
+msgstr ""
+
+#: templates/stats/statuses.html:17
+#, fuzzy
+msgid "Orders per user"
+msgstr "Arbet"
+
+#: templates/stats/statuses.html:18
+msgid ""
+"Shows the number of orders with a particular status per each user at the "
+"given location that have been assigned to a technician over the specified "
+"time period."
+msgstr ""
+
+#: views/account.py:26
+msgid "Profile Settings"
+msgstr ""
+
+#: views/account.py:34 views/admin.py:235
+msgid "Settings saved"
+msgstr ""
+
+#: views/account.py:47
+msgid "Error in user details"
+msgstr ""
+
+#: views/account.py:73
+msgid "My Orders"
+msgstr ""
+
+#: views/account.py:81
+msgid "Sign In"
+msgstr ""
+
+#: views/account.py:102
+#, python-format
+msgid "%s logged in"
+msgstr ""
+
+#: views/account.py:109
+msgid "Incorrect username or password"
+msgstr ""
+
+#: views/account.py:111
+msgid "Login failed"
+msgstr ""
+
+#: views/account.py:119
+msgid "You have logged out"
+msgstr ""
+
+#: views/account.py:223
+msgid "Users can only delete their own calendars!"
+msgstr ""
+
+#: views/account.py:229
+msgid "Calendar deleted"
+msgstr ""
+
+#: views/account.py:232
+msgid "Really delete this calendar?"
+msgstr ""
+
+#: views/account.py:251
+msgid "Calendar saved"
+msgstr ""
+
+#: views/account.py:274
+msgid "Calendar event created"
+msgstr ""
+
+#: views/account.py:284
+msgid "Event saved"
+msgstr ""
+
+#: views/account.py:287
+msgid "Edit Event"
+msgstr ""
+
+#: views/account.py:298
+msgid "Calendar event updated"
+msgstr ""
+
+#: views/account.py:305
+msgid "Users can only delete their own events!"
+msgstr ""
+
+#: views/account.py:313
+msgid "Calendar event deleted"
+msgstr ""
+
+#: views/account.py:316
+msgid "Really delete this event?"
+msgstr ""
+
+#: views/account.py:340
+msgid "Your registration is now pending approval."
+msgstr ""
+
+#: views/account.py:354
+msgid "All notifications cleared"
+msgstr ""
+
+#: views/account.py:365 views/order.py:855
+msgid "Search query is too short"
+msgstr ""
+
+#: views/account.py:381
+msgid "Search results"
+msgstr ""
+
+#: views/admin.py:65 views/admin.py:270 views/customer.py:140
+#: views/device.py:152
+#, python-format
+msgid "%s saved"
+msgstr ""
+
+#: views/admin.py:71
+msgid "GSX account for this sold-to and environment already exists"
+msgstr ""
+
+#: views/admin.py:83
+msgid "GSX account deleted"
+msgstr ""
+
+#: views/admin.py:126
+msgid "Checklist saved"
+msgstr ""
+
+#: views/admin.py:138
+msgid "Checklist deleted"
+msgstr ""
+
+#: views/admin.py:142
+msgid "Really delete this checklist?"
+msgstr ""
+
+#: views/admin.py:143
+msgid "This will also delete all checklist values."
+msgstr ""
+
+#: views/admin.py:179
+#, python-format
+msgid "Tag %s saved"
+msgstr ""
+
+#: views/admin.py:194
+msgid "Tag deleted"
+msgstr ""
+
+#: views/admin.py:197
+msgid "Really delete this tag?"
+msgstr ""
+
+#: views/admin.py:215
+msgid "Check your settings"
+msgstr ""
+
+#: views/admin.py:283 views/admin.py:542 views/customer.py:154
+#, python-format
+msgid "%s deleted"
+msgstr ""
+
+#: views/admin.py:322
+msgid "Field saved"
+msgstr ""
+
+#: views/admin.py:334
+msgid "Field deleted"
+msgstr ""
+
+#: views/admin.py:337
+msgid "Really delete this field?"
+msgstr ""
+
+#: views/admin.py:367
+#, python-format
+msgid "Template %s saved"
+msgstr ""
+
+#: views/admin.py:383
+#, python-format
+msgid "Template %s deleted"
+msgstr ""
+
+#: views/admin.py:386
+msgid "Really delete this template?"
+msgstr ""
+
+#: views/admin.py:427
+msgid "Group saved"
+msgstr ""
+
+#: views/admin.py:439
+msgid "Group deleted"
+msgstr ""
+
+#: views/admin.py:454
+msgid "User deleted"
+msgstr ""
+
+#: views/admin.py:483
+#, python-format
+msgid "User %s saved"
+msgstr ""
+
+#: views/admin.py:486
+msgid "Error in user profile data"
+msgstr ""
+
+#: views/admin.py:492
+#, python-format
+msgid "%d users"
+msgstr ""
+
+#: views/admin.py:527
+#, python-format
+msgid "Location %s saved"
+msgstr ""
+
+#: views/admin.py:548
+msgid "Really delete this location?"
+msgstr ""
+
+#: views/admin.py:549
+msgid "This will not delete the orders at this location"
+msgstr ""
+
+#: views/admin.py:560
+msgid "Create, edit and delete service queues"
+msgstr ""
+
+#: views/admin.py:587
+msgid "Failed to save queue"
+msgstr ""
+
+#: views/admin.py:594
+#, python-format
+msgid "%s queue saved"
+msgstr ""
+
+#: views/admin.py:611
+msgid "Queue deleted"
+msgstr ""
+
+#: views/admin.py:613
+msgid "Cannot delete queue"
+msgstr ""
+
+#: views/admin.py:633 views/admin.py:644
+msgid "Access denied"
+msgstr ""
+
+#: views/admin.py:637
+msgid "Manage Sites"
+msgstr ""
+
+#: views/admin.py:663
+msgid "Site saved"
+msgstr ""
+
+#: views/admin.py:682
+#, python-format
+msgid "%d users imported"
+msgstr ""
+
+#: views/checkin.py:109
+msgid "Welcome"
+msgstr "Välkommen"
+
+#: views/checkin.py:120
+msgid "1/3: Enter serial number, IMEI code or choose a device"
+msgstr "1/3: Ange serienummer, IMEI-kod eller välj en enhet"
+
+#: views/checkin.py:123
+msgid "Please enable cookies in your browser"
+msgstr "Vänligen aktivera cookies i din webbläsare"
+
+#: views/checkin.py:134 views/checkin.py:161
+msgid "Your session has expired, please try again."
+msgstr "Din session har gått ut, var god försök igen."
+
+#: views/checkin.py:140
+msgid "The serial number you entered is not valid."
+msgstr "Serienumret du angav är ogiltigt."
+
+#: views/checkin.py:142
+msgid ""
+"Couldn't check warranty status, please choose device type manually or try "
+"again later."
+msgstr "Det gick inte att kontrollera garantistatus, försök igen senare."
+
+#: views/checkin.py:155
+msgid "1/3: Device details"
+msgstr "1/3: Enhetsinformation"
+
+#: views/checkin.py:192
+msgid "2/3: Problem description"
+msgstr "2/3: Problembeskrivning"
+
+#: views/checkin.py:206
+msgid "3/3: Your contact details"
+msgstr "3/3: Dina kontaktuppgifter"
+
+#: views/checkin.py:234
+msgid "Order does not exist"
+msgstr "Beställnings existerar inte"
+
+#: views/checkin.py:244
+msgid "2/3: Troubleshooting"
+msgstr "2/3: Felsökning"
+
+#: views/checkin.py:281
+msgid "1/3: Choose your device"
+msgstr "1/3: Välj din enhet"
+
+#: views/checkin.py:284
+msgid "Apple Keyboard"
+msgstr "Apple tangentbord"
+
+#: views/checkin.py:285
+msgid "Apple Wireless Device"
+msgstr "Apple trådlös enhet"
+
+#: views/checkin.py:289
+#, python-format
+msgid "1/3: %s"
+msgstr "1/3: %s"
+
+#: views/checkin.py:320
+msgid "Repair Status"
+msgstr "Reparationsstatus"
+
+#: views/checkin.py:332
+msgid "Waiting to be processed"
+msgstr "Väntar på att behandlas"
+
+#: views/checkin.py:334
+#, python-format
+msgid "Order %s not found"
+msgstr "Arbet %s hittades inte"
+
+#: views/checkin.py:359
+msgid "Your session has expired"
+msgstr "Din session har gått ut"
+
+#: views/customer.py:89
+msgid "Customer added"
+msgstr "Kunden sattes"
+
+#: views/customer.py:105
+msgid "Customer not found"
+msgstr "Kunden hittades inte"
+
+#: views/customer.py:206
+msgid "Customer saved"
+msgstr "Kunden sparas"
+
+#: views/customer.py:232
+msgid "Customer deleted"
+msgstr "Kunden utgår"
+
+#: views/customer.py:250
+#, python-format
+msgid "Merge %s with"
+msgstr ""
+
+#: views/customer.py:265
+msgid "Customer records merged succesfully"
+msgstr ""
+
+#: views/customer.py:281
+#, python-format
+msgid "Customer %s moved to top level"
+msgstr ""
+
+#: views/customer.py:285
+#, python-format
+msgid "Customer %(customer)s moved to %(target)s"
+msgstr ""
+
+#: views/customer.py:307 views/device.py:282 views/order.py:875
+#: views/product.py:325 views/search.py:35
+#, python-format
+msgid "Search results for \"%s\""
+msgstr ""
+
+#: views/customer.py:357
+msgid "Search for customers"
+msgstr "Sök för kunder"
+
+#: views/customer.py:376
+msgid "Please specify search query first"
+msgstr ""
+
+#: views/customer.py:419
+msgid "Invalid upload data"
+msgstr ""
+
+#: views/customer.py:439
+#, python-format
+msgid "%d customer(s) imported"
+msgstr ""
+
+#: views/device.py:114
+msgid "Device deleted"
+msgstr ""
+
+#: views/device.py:116
+msgid "Cannot delete device with GSX repairs"
+msgstr ""
+
+#: views/device.py:314
+#, python-format
+msgid "Devices matching \"%s\""
+msgstr ""
+
+#: views/device.py:333
+msgid "Device search"
+msgstr ""
+
+#: views/device.py:386
+msgid "Invalid serial number for parts lookup"
+msgstr ""
+
+#: views/device.py:389
+msgid "Error calculating prices. Please check your system settings."
+msgstr ""
+
+#: views/device.py:486
+#, python-format
+msgid "%d devices imported"
+msgstr ""
+
+#: views/device.py:499
+msgid "Warranty status updated successfully"
+msgstr ""
+
+#: views/error.py:24
+#, python-format
+msgid "Browser: %s"
+msgstr ""
+
+#: views/gsx.py:21
+#, python-format
+msgid "Part %s updated"
+msgstr ""
+
+#: views/gsx.py:56
+#, python-format
+msgid "Part %(part)s added to repair %(repair)s"
+msgstr ""
+
+#: views/gsx.py:85
+#, python-format
+msgid "Part %(part)s removed from %(repair)s"
+msgstr ""
+
+#: views/gsx.py:99
+msgid "Submitted repairs cannot be deleted"
+msgstr ""
+
+#: views/gsx.py:105
+msgid "GSX repair deleted"
+msgstr ""
+
+#: views/gsx.py:147
+msgid "Submitted repairs cannot be edited"
+msgstr ""
+
+#: views/gsx.py:150
+msgid "Please add some parts before creating repair"
+msgstr ""
+
+#: views/gsx.py:153
+msgid "Cannot create GSX repair without valid customer data"
+msgstr ""
+
+#: views/gsx.py:190
+msgid "GSX repair saved"
+msgstr ""
+
+#: views/gsx.py:216
+msgid "Invalid component data"
+msgstr ""
+
+#: views/gsx.py:231
+msgid "Invalid customer info"
+msgstr ""
+
+#: views/gsx.py:296
+#, python-format
+msgid "%s serial numbers updated"
+msgstr ""
+
+#: views/invoices.py:91
+#, python-format
+msgid "Receipt #%d"
+msgstr "Kvitto #%d"
+
+#: views/invoices.py:99
+#, python-format
+msgid "Invoice %s"
+msgstr "Kvitto %s"
+
+#: views/invoices.py:110
+#, python-format
+msgid "Dispatch Order %s"
+msgstr ""
+
+#: views/invoices.py:135
+msgid "Walk-In Customer"
+msgstr ""
+
+#: views/note.py:170
+#, python-format
+msgid "Re: %s"
+msgstr ""
+
+#: views/note.py:237
+msgid "Note deleted"
+msgstr ""
+
+#: views/note.py:305
+msgid "OK"
+msgstr ""
+
+#: views/note.py:329
+#, python-format
+msgid "Notes containing \"%s\""
+msgstr ""
+
+#: views/note.py:373
+msgid "Message search"
+msgstr ""
+
+#: views/note.py:384
+msgid "Edit Escalation"
+msgstr ""
+
+#: views/order.py:142
+#, python-format
+msgid "%d search results"
+msgstr ""
+
+#: views/order.py:153
+#, python-format
+msgid "Order %s"
+msgstr "Arbet %s"
+
+#: views/order.py:212
+#, python-format
+msgid "Order %s reopened"
+msgstr ""
+
+#: views/order.py:349
+#, python-format
+msgid "Repair %s marked complete."
+msgstr ""
+
+#: views/order.py:404
+#, python-format
+msgid "Order %s deleted"
+msgstr ""
+
+#: views/order.py:408
+#, python-format
+msgid "Cannot delete order %(order)s: %(error)s"
+msgstr ""
+
+#: views/order.py:418
+msgid "Follow"
+msgstr ""
+
+#: views/order.py:424
+msgid "Unfollow"
+msgstr ""
+
+#: views/order.py:461
+msgid "Closed orders cannot be updated"
+msgstr ""
+
+#: views/order.py:475
+#, python-format
+msgid "User %s does not exist"
+msgstr ""
+
+#: views/order.py:503
+#, python-format
+msgid "Label %s does not exist"
+msgstr ""
+
+#: views/order.py:508 views/order.py:514 views/order.py:523
+msgid "Order updated"
+msgstr ""
+
+#: views/order.py:520
+#, python-format
+msgid "Order moved to %s"
+msgstr ""
+
+#: views/order.py:536
+#, python-format
+msgid "Service Order #%s"
+msgstr ""
+
+#: views/order.py:633
+#, python-format
+msgid "Products of order %s reserved"
+msgstr ""
+
+#: views/order.py:674 views/product.py:271
+#, python-format
+msgid "Product %s saved"
+msgstr ""
+
+#: views/order.py:761
+#, fuzzy
+msgid "Order item does not exist"
+msgstr "Beställnings existerar inte"
+
+#: views/order.py:842
+#, python-format
+msgid "Customer %s removed"
+msgstr ""
+
+#: views/product.py:117
+msgid "Parts database uploaded for processing"
+msgstr ""
+
+#: views/product.py:206
+#, python-format
+msgid "%d products imported"
+msgstr ""
+
+#: views/product.py:211
+msgid "Upload products"
+msgstr ""
+
+#: views/product.py:274
+msgid "Error in inventory details"
+msgstr ""
+
+#: views/product.py:276
+msgid "Error in product info"
+msgstr ""
+
+#: views/product.py:296
+msgid "Product deleted"
+msgstr ""
+
+#: views/product.py:298
+msgid "Cannot delete product"
+msgstr ""
+
+#: views/product.py:371
+#, python-format
+msgid "Category %s already exists"
+msgstr ""
+
+#: views/product.py:373
+#, python-format
+msgid "Category %s saved"
+msgstr ""
+
+#: views/product.py:389
+msgid "Category deleted"
+msgstr ""
+
+#: views/product.py:437
+msgid "Price info updated from GSX"
+msgstr ""
+
+#: views/product.py:439
+msgid "Failed to update price from GSX"
+msgstr ""
+
+#: views/purchases.py:81
+#, python-format
+msgid "Product %s removed"
+msgstr ""
+
+#: views/purchases.py:96
+#, python-format
+msgid "Purchase Order %d"
+msgstr ""
+
+#: views/purchases.py:132
+#, python-format
+msgid "Purchase Order %d saved"
+msgstr ""
+
+#: views/purchases.py:136
+#, python-format
+msgid "Purchase Order %d submitted"
+msgstr ""
+
+#: views/purchases.py:144
+#, python-format
+msgid "Purchase Order #%d"
+msgstr ""
+
+#: views/purchases.py:159
+#, python-format
+msgid "Purchase Order %s has already been submitted"
+msgstr ""
+
+#: views/purchases.py:177
+#, python-format
+msgid "Products ordered with confirmation %s"
+msgstr ""
+
+#: views/purchases.py:193
+#, python-format
+msgid "Purchase Order %s deleted"
+msgstr ""
+
+#: views/shipments.py:83
+#, python-format
+msgid "%d incoming products"
+msgstr ""
+
+#: views/shipments.py:113
+#, python-format
+msgid "%d products received"
+msgstr ""
+
+#: views/shipments.py:152
+#, python-format
+msgid "Product %s received"
+msgstr ""
+
+#: views/shipments.py:186
+msgid "Browse Bulk Returns"
+msgstr ""
+
+#: views/shipments.py:210
+msgid "View bulk return"
+msgstr ""
+
+#: views/shipments.py:223
+#, python-format
+msgid "Location %s has no Ship-To"
+msgstr ""
+
+#: views/shipments.py:246
+msgid "Bulk return saved"
+msgstr ""
+
+#: views/shipments.py:251
+#, python-format
+msgid "Bulk return %s submitted"
+msgstr ""
+
+#: views/shipments.py:266
+#, python-format
+msgid "%d parts pending return"
+msgstr ""
+
+#: views/shipments.py:279
+#, python-format
+msgid "Part %s removed from bulk return"
+msgstr ""
+
+#: views/shipments.py:296
+#, python-format
+msgid "Part %s added to return"
+msgstr ""
+
+#: views/shipments.py:322
+msgid "Return DOA Part"
+msgstr ""
+
+#: views/shipments.py:331
+msgid "This part will be converted to regular inventory"
+msgstr ""
+
+#: views/shipments.py:346
+msgid "Part updated"
+msgstr ""
+
+#: views/stats.py:241
+msgid "Time Scale"
+msgstr ""
+
+#: views/stats.py:465
+msgid "No Queue"
+msgstr ""
diff --git a/servo/management/__init__.py b/servo/management/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/management/__init__.py
diff --git a/servo/management/commands/__init__.py b/servo/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/management/commands/__init__.py
diff --git a/servo/management/commands/backup.py b/servo/management/commands/backup.py
new file mode 100644
index 0000000..90c2bd0
--- /dev/null
+++ b/servo/management/commands/backup.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import os
+import csv
+import shutil
+import subprocess
+from datetime import date
+from django.db import connection
+
+from django.core.management.base import BaseCommand
+
+
+def write(path, header, cursor):
+ with open(path, 'w') as fout:
+ writer = csv.writer(fout)
+ writer.writerow(header)
+
+ for row in cursor.fetchall():
+ row = [unicode(s).encode('utf-8') for s in row]
+ writer.writerow(row)
+
+
+class Command(BaseCommand):
+
+ help = 'Export this servo database in a portable format'
+
+ def handle(self, *args, **options):
+ backupdir = 'backups/%s' % date.today().isoformat()
+ os.mkdir(backupdir)
+
+ cursor = connection.cursor()
+
+ path = os.path.join(backupdir, 'notes.csv')
+ cursor.execute("""SELECT id, order_id, created_by_id, created_at, body
+ FROM servo_note""")
+ header = ['ID', 'ORDER_ID', 'USER_ID', 'CREATED_AT', 'NOTE']
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'users.csv')
+ header = ['ID', 'USERNAME', 'FIRST_NAME', 'LAST_NAME', 'EMAIL']
+ cursor.execute("""SELECT id, username, first_name, last_name, email
+ FROM servo_user WHERE is_visible = TRUE""")
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'orders.csv')
+ header = ['ID', 'CODE', 'CREATED_AT',
+ 'CLOSED_AT', 'CUSTOMER_ID', 'USER_ID', 'QUEUE_ID']
+ cursor.execute("""SELECT id, code, created_at, closed_at,
+ customer_id, user_id, queue_id
+ FROM servo_order""")
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'queues.csv')
+ header = ['ID', 'NAME', 'DESCRIPTION',
+ 'CLOSED_AT', 'CUSTOMER_ID', 'USER_ID', 'QUEUE_ID']
+ cursor.execute("""SELECT id, title, description FROM servo_queue""")
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'devices.csv')
+ header = ['ID', 'SERIAL_NUMBER', 'IMEI',
+ 'CONFIGURATION', 'WARRANTY_STATUS', 'PURCHASE_DATE', 'NOTES']
+ cursor.execute("""SELECT id, sn, imei, configuration, warranty_status,
+ purchased_on, notes FROM servo_device""")
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'repairs.csv')
+ header = ['ID', 'ORDER_ID', 'DEVICE_ID', 'USER_ID',
+ 'SUBMITTED_AT', 'COMPLETED_AT', 'REQUEST_REVIEW',
+ 'TECH_ID', 'UNIT_RECEIVED', 'CONFIRMATION',
+ 'REFERENCE', 'SYMPTOM', 'DIAGNOSIS', 'NOTES']
+ cursor.execute("""SELECT id, order_id, device_id,
+ created_by_id, submitted_at, completed_at,
+ request_review, tech_id, unit_received_at, confirmation, reference,
+ symptom, diagnosis, notes
+ FROM servo_repair
+ WHERE submitted_at IS NOT NULL""")
+ write(path, header, cursor)
+
+ header = ['ID', 'CODE', 'TITLE', 'DESCRIPTION',
+ 'PRICE_PURCHASE_EXCHANGE', 'PRICE_PURCHASE_STOCK',
+ 'PRICE_SALES_EXCHANGE', 'PRICE_SALES_STOCK', 'COMPONENT_CODE',
+ 'PART_TYPE', 'EEE_CODE']
+ cursor.execute("""SELECT id, code, title, description,
+ price_purchase_exchange, price_purchase_stock,
+ price_sales_exchange, price_sales_stock,
+ component_code, part_type, eee_code
+ FROM servo_product""")
+ path = os.path.join(backupdir, 'products.csv')
+ write(path, header, cursor)
+
+ header = ['ID', 'PARENT_ID', 'NAME', 'PHONE', 'EMAIL',
+ 'STREET_ADDRESS', 'POSTAL_CODE', 'CITY'
+ 'COUNTRY', 'NOTES']
+ cursor.execute("""SELECT id, parent_id, name, phone,
+ email, street_address, zip_code, city, country, notes
+ FROM servo_customer""")
+ path = os.path.join(backupdir, 'customers.csv')
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'order_products.csv')
+ header = ['ID', 'PRODUCT_ID', 'ORDER_ID', 'CODE', 'TITLE',
+ 'DESCRIPTION', 'AMOUNT', 'SERIAL_NUMBER', 'KBB_SN',
+ 'IMEI', 'REPORTED', 'PRICE_CATEGORY', 'PRICE'
+ 'COMPTIA_CODE', 'COMPTIA_MODIFIER']
+ cursor.execute("""SELECT id, product_id, order_id, code,
+ title, description, amount, sn, price, kbb_sn,
+ imei, should_report, price_category, price,
+ comptia_code, comptia_modifier
+ FROM servo_serviceorderitem""")
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'parts.csv')
+ header = ['ID', 'REPAIR_ID', 'ORDER_ITEM_ID',
+ 'NUMBER', 'TITLE', 'COMPTIA_CODE', 'COMPTIA_MODIFIER',
+ 'RETURN_ORDER', 'RETURN_STATUS', 'RETURN_CODE',
+ 'ORDER_STATUS', 'COVERAGE', 'SHIP_TO', 'RETURNED_AT']
+ cursor.execute("""SELECT id, repair_id, order_item_id,
+ part_number, part_title, comptia_code, comptia_modifier,
+ return_order, return_status, return_code,
+ order_status, coverage_description, ship_to, returned_at
+ FROM servo_servicepart""")
+ write(path, header, cursor)
+
+ path = os.path.join(backupdir, 'order_devices.csv')
+ header = ['ID', 'ORDER_ID', 'DEVICE_ID', 'REPORTED']
+ cursor.execute("""SELECT id, order_id, device_id, should_report
+ FROM servo_orderdevice""")
+ write(path, header, cursor)
+
+ subprocess.call(['tar', '-C', backupdir, '-zcf', '%s.tar.gz' % backupdir, '.'])
+ shutil.rmtree(backupdir, ignore_errors=True)
diff --git a/servo/management/commands/checkescalations.py b/servo/management/commands/checkescalations.py
new file mode 100644
index 0000000..7a49244
--- /dev/null
+++ b/servo/management/commands/checkescalations.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.db.models import Q
+from django.utils import timezone
+from django.core.management.base import BaseCommand, CommandError
+
+from servo.models import Configuration, Note, User, Escalation
+
+
+class Command(BaseCommand):
+ help = "Check updates for open escalations"
+
+ def handle(self, *args, **options):
+ uid = Configuration.conf('imap_act')
+
+ if uid in [None, '']:
+ return
+
+ user = User.objects.get(pk=uid)
+ tz = timezone.get_current_timezone()
+
+ for i in Escalation.objects.exclude(Q(escalation_id='') | Q(status='C')):
+ i.gsx_account.connect(i.created_by)
+ r = i.get_escalation().lookup()
+ aware = timezone.make_aware(r.lastModifiedTimestamp, tz)
+
+ if aware < i.updated_at: # hasn't been updated
+ continue
+
+ try:
+ parent = i.note_set.latest()
+ except Note.DoesNotExist:
+ continue
+
+ bodies = [n.body for n in i.note_set.all()]
+
+ for x in r.escalationNotes.iterchildren():
+ if x.text in bodies: # skip notes we already have
+ continue
+
+ note = Note(created_by=user, escalation=i, body=x.text)
+ parent.add_reply(note)
+ note.save()
+
+ i.updated_at = timezone.now()
+ i.status = r.escalationStatus
+ i.save()
diff --git a/servo/management/commands/checkmail.py b/servo/management/commands/checkmail.py
new file mode 100644
index 0000000..891d6aa
--- /dev/null
+++ b/servo/management/commands/checkmail.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import logging
+from email.parser import Parser
+
+from django.core.management.base import BaseCommand
+
+from servo.models import Configuration, Note, User
+
+
+class Command(BaseCommand):
+ help = "Checks IMAP box for new mail"
+
+ def handle(self, *args, **options):
+ uid = Configuration.conf('imap_act')
+
+ if uid == '':
+ raise ValueError('Incoming message user not configured')
+
+ user = User.objects.get(pk=uid)
+ server = Configuration.get_imap_server()
+ typ, data = server.search(None, "UnSeen")
+
+ for num in data[0].split():
+ #logging.debug("** Processing message %s" % num)
+ typ, data = server.fetch(num, "(RFC822)")
+ # parsestr() seems to return an email.message?
+ msg = Parser().parsestr(data[0][1])
+ Note.from_email(msg, user)
+ #server.copy(num, 'servo')
+ server.store(num, '+FLAGS', '\\Seen')
+
+ server.close()
+ server.logout()
diff --git a/servo/management/commands/cleandups.py b/servo/management/commands/cleandups.py
new file mode 100644
index 0000000..fa333bf
--- /dev/null
+++ b/servo/management/commands/cleandups.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import logging
+from django.core.management.base import BaseCommand
+from django.template.defaultfilters import slugify
+
+from servo.models import TaggedItem
+
+
+class Command(BaseCommand):
+
+ help = "Cleans various duplicate data"
+
+ def handle(self, *args, **options):
+ logging.info("Cleaning up duplicate tags")
+ for d in TaggedItem.objects.filter(slug=None):
+ d.slug = slugify(d.description)
+ d.save()
diff --git a/servo/management/commands/cleanphones.py b/servo/management/commands/cleanphones.py
new file mode 100644
index 0000000..4bc2371
--- /dev/null
+++ b/servo/management/commands/cleanphones.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+
+from servo.models import Customer
+
+
+class Command(BaseCommand):
+ help = "Cleans illegal characters from phone numbers"
+
+ def handle(self, *args, **options):
+ ALLOWED_CHARS = r'^[\d\s\+\-]+$'
+ for i in Customer.objects.exclude(phone__regex=ALLOWED_CHARS):
+ if i.phone == '':
+ continue
+
+ i.notes = i.notes + i.phone
+ i.phone = ''
+ i.save()
diff --git a/servo/management/commands/cleanup.py b/servo/management/commands/cleanup.py
new file mode 100644
index 0000000..8f6f39e
--- /dev/null
+++ b/servo/management/commands/cleanup.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import os
+import Image
+import logging
+from glob import glob
+from django.core.management.base import BaseCommand
+
+from servo.models import Attachment
+
+
+class Command(BaseCommand):
+
+ help = "Does various cleanup"
+
+ def handle(self, *args, **options):
+ size = 128, 128
+ logging.info("Building avatar thumbnails")
+ for infile in glob("servo/uploads/avatars/*.jpg"):
+ logging.info(infile)
+ im = Image.open(infile)
+ im.thumbnail(size, Image.ANTIALIAS)
+ im.save(infile, "JPEG")
+
+ logging.info("Cleaning up unused attachments")
+ for infile in glob("servo/uploads/attachments/*"):
+ fn = infile.decode('utf-8')
+ fp = os.path.join("attachments", os.path.basename(fn))
+ try:
+ Attachment.objects.get(content=fp)
+ except Attachment.DoesNotExist:
+ os.remove(infile)
diff --git a/servo/management/commands/clearcache.py b/servo/management/commands/clearcache.py
new file mode 100644
index 0000000..826ddef
--- /dev/null
+++ b/servo/management/commands/clearcache.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.cache import cache
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+
+ help = "Clears this install's cache"
+
+ def handle(self, *args, **options):
+ cache.clear()
diff --git a/servo/management/commands/cron.py b/servo/management/commands/cron.py
new file mode 100755
index 0000000..e2831e9
--- /dev/null
+++ b/servo/management/commands/cron.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+from datetime import date, timedelta
+
+from django.conf import settings
+from django.core.files import File
+from django.utils.translation import ugettext as _
+
+from django.core.management.base import BaseCommand
+
+from django.db.models import F
+from django.utils import timezone
+from django.core.cache import cache
+
+from servo.models.common import Configuration, Location, CsvTable, GsxAccount
+from servo.models import Inventory, Order, PurchaseOrder, User
+
+
+def send_table(sender, recipient, subject, table, send_empty=False):
+ from django.core.mail import send_mail
+ if not send_empty and not table.has_body():
+ return
+
+ config = Configuration.conf()
+ settings.EMAIL_HOST = config.get('smtp_host')
+ settings.EMAIL_USE_TLS = config.get('smtp_ssl')
+ settings.EMAIL_HOST_USER = config.get('smtp_user')
+ settings.EMAIL_HOST_PASSWORD = config.get('smtp_password')
+
+ send_mail(subject, unicode(table), sender, [recipient], fail_silently=False)
+
+
+class Command(BaseCommand):
+ help = "Runs Servo's periodic commands"
+
+ def update_invoices(self):
+ uid = Configuration.conf('imap_act')
+ user = User.objects.get(pk=uid)
+
+ for a in GsxAccount.objects.all():
+ try:
+ a.default(user)
+ lookup = gsxws.Lookup(shipTo=a.ship_to, invoiceDate=date.today())
+ invoices = lookup.invoices()
+ for i in invoices:
+ details = gsxws.Lookup(invoiceID=i).invoice_details()
+ # @TODO: What about stock orders?
+ po = PurchaseOrder.objects.get(pk=details.purchaseOrderNumber)
+ po.invoice_id = i
+ po.invoice.save("%s.pdf" % i, File(open(details.invoiceData)))
+ except Exception, e:
+ raise e
+
+ def update_warranty(self):
+ pass
+
+ def notify_aging_repairs(self):
+ """
+ Reports on cases that have been red for a day
+ """
+ conf = Configuration.conf()
+
+ try:
+ sender = conf['default_sender']
+ except KeyError:
+ raise ValueError('Default sender address not defined')
+
+ now = timezone.now()
+ limit = now - timedelta(days=1)
+ locations = Location.objects.filter(site_id=settings.SITE_ID)
+
+ for l in locations:
+ table = CsvTable()
+ table.addheader(['Order', 'Assigned To', 'Status', 'Days red'])
+
+ # "Aging" repairs are ones that have been red for at least a day
+ orders = Order.objects.filter(
+ location=l,
+ state__lt=Order.STATE_CLOSED,
+ status_limit_yellow__lt=limit
+ )
+
+ for o in orders:
+ username = o.get_user_name() or _("Unassigned")
+ status_title = o.get_status_name() or _("No Status")
+ days = (now - o.status_limit_yellow).days
+ table.addrow([o.code, username, status_title, days])
+
+ subject = _(u"Repairs aging beyond limits at %s") % l.title
+
+ if Configuration.notify_location():
+ send_table(sender, l.email, subject, table)
+ if Configuration.notify_email_address():
+ send_table(sender, conf['notify_address'], subject, table)
+
+ def notify_stock_limits(self):
+ conf = Configuration.conf()
+
+ try:
+ sender = conf['default_sender']
+ except KeyError:
+ raise ValueError('Default sender address not defined')
+
+ locations = Location.objects.filter(site_id=settings.SITE_ID)
+
+ for l in locations:
+ out_of_stock = Inventory.objects.filter(
+ location=l,
+ amount_stocked__lt=F('amount_minimum')
+ )
+
+ table = CsvTable()
+ table.addheader(['Product', 'Minimum', 'Stocked'])
+
+ for i in out_of_stock:
+ table.addrow([i.product.code, i.amount_minimum, i.amount_stocked])
+
+ subject = _(u"Products stocked below limit")
+
+ if Configuration.notify_location():
+ send_table(sender, l.email, subject, table)
+ if Configuration.notify_email_address():
+ send_table(sender, conf['notify_address'], subject, table)
+
+ def update_counts(self):
+ now = timezone.now()
+ orders = Order.objects.filter(state__lt=Order.STATE_CLOSED)
+ green = orders.filter(status_limit_green__gte=now)
+ cache.set('green_order_count', green.count())
+ yellow = orders.filter(status_limit_yellow__gte=now)
+ cache.set('yellow_order_count', yellow.count())
+ red = orders.filter(status_limit_yellow__lte=now)
+ cache.set('red_order_count', red.count())
+
+ def handle(self, *args, **options):
+ #self.update_invoices()
+ self.update_counts()
+ self.notify_aging_repairs()
+ self.notify_stock_limits()
diff --git a/servo/management/commands/dbbackup.py b/servo/management/commands/dbbackup.py
new file mode 100644
index 0000000..91d3875
--- /dev/null
+++ b/servo/management/commands/dbbackup.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import sys
+import logging
+import subprocess
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ help = 'Backup this Servo database'
+ def handle(self, *args, **options):
+ if len(args) < 1:
+ print 'Usage: dbbackup file'
+ sys.exit(1)
+ db = settings.DATABASES['default']
+ pg_dump = subprocess.check_output(['which', 'pg_dump']).strip()
+ subprocess.call([pg_dump, '-Fc', db['NAME'], '-U', db['USER'],
+ '-f' , args[0]], env={'PGPASSWORD': db['PASSWORD']})
diff --git a/servo/management/commands/dbdump.py b/servo/management/commands/dbdump.py
new file mode 100644
index 0000000..12023c4
--- /dev/null
+++ b/servo/management/commands/dbdump.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import subprocess
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ help = "Dumps DB of this instance to specified file"
+
+ def handle(self, *args, **options):
+ dbname = settings.DATABASES['default']['NAME']
+ #subprocess.call('pg_dump', '-Fc', dbname, '-U', 'pgsql' > "${BACKUPDIR}/${db}_$(date "+%Y%m%d_%H%M").pgdump"
diff --git a/servo/management/commands/dbrestore.py b/servo/management/commands/dbrestore.py
new file mode 100644
index 0000000..640d3d6
--- /dev/null
+++ b/servo/management/commands/dbrestore.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import subprocess
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ help = 'Restore this Servo database from backup'
+ def handle(self, *args, **options):
+ if len(args) < 1:
+ print 'Usage: dbrestore file'
+ sys.exit(1)
+
+ db = settings.DATABASES['default']
+ pg_restore = subprocess.check_output(['which', 'pg_restore']).strip()
+ subprocess.call([pg_restore, '-d', db['NAME'], '-O', '-x', '-U', db['USER'],
+ args[0]], env={'PGPASSWORD': db['PASSWORD']})
diff --git a/servo/management/commands/deleteorders.py b/servo/management/commands/deleteorders.py
new file mode 100644
index 0000000..e531029
--- /dev/null
+++ b/servo/management/commands/deleteorders.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import logging
+from django.core.management.base import BaseCommand
+
+from servo.models import (Order, Repair,)
+
+class Command(BaseCommand):
+ help = "Deletes orders listed in text file"
+
+ def handle(self, *args, **options):
+ if len(args) < 1:
+ print 'Usage: deleteorders data.txt'
+ sys.exit(1)
+
+ counter = 0
+ dataf = open(args[0], 'r')
+
+ for l in dataf.readlines():
+ cols = l.strip().split("\t")
+ try:
+ print 'Deleting order %s' % cols[0]
+ order = Order.objects.get(code=cols[0])
+ Repair.objects.filter(order=order).delete()
+ order.delete()
+ counter += 1
+ except Order.DoesNotExist:
+ pass
+
+ print '%d orders deleted' % counter
diff --git a/servo/management/commands/fixfollowers.py b/servo/management/commands/fixfollowers.py
new file mode 100644
index 0000000..bd8e1cb
--- /dev/null
+++ b/servo/management/commands/fixfollowers.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+
+from servo.models import Event
+
+
+class Command(BaseCommand):
+ help = "Fixes missing follower data from event logs"
+
+ def handle(self, *args, **options):
+ for i in Event.objects.filter(action='set_user'):
+ user = i.triggered_by
+ order = i.content_object
+ if order.user is None and 'unassigned' not in i.description:
+ print('Assigning %s to %s' % (order, user))
+ order.add_follower(user)
+ order.user = i.triggered_by
+ order.save()
diff --git a/servo/management/commands/fixproducts.py b/servo/management/commands/fixproducts.py
new file mode 100644
index 0000000..9767ab4
--- /dev/null
+++ b/servo/management/commands/fixproducts.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+from django.template.defaultfilters import slugify
+
+from servo.models import ServiceOrderItem, Product
+
+
+class Command(BaseCommand):
+ help = "Fixes SOI and Product codes that include spaces"
+
+ def handle(self, *args, **options):
+ for i in Product.objects.filter(code__contains=' '):
+ i.code = slugify(i.code)
+ i.save()
+
+ for i in ServiceOrderItem.objects.filter(code__contains=' '):
+ i.code = slugify(i.code)
+ i.save()
diff --git a/servo/management/commands/fixtimestamps.py b/servo/management/commands/fixtimestamps.py
new file mode 100644
index 0000000..81e1cdb
--- /dev/null
+++ b/servo/management/commands/fixtimestamps.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+from django.template.defaultfilters import slugify
+from django.contrib.contenttypes.models import ContentType
+
+from servo.models import OrderStatus
+
+
+class Command(BaseCommand):
+ help = "Fixes finished_at timestamps of OrderStatuses"
+
+ def handle(self, *args, **options):
+ for s in OrderStatus.objects.all():
+ next_status = s.get_next()
+
+ if next_status is None:
+ continue # current status
+
+ s.finished_at = next_status.started_at
+ s.duration = (s.finished_at - s.started_at).total_seconds()
+ s.save()
diff --git a/servo/management/commands/importparts.py b/servo/management/commands/importparts.py
new file mode 100644
index 0000000..8df1ecb
--- /dev/null
+++ b/servo/management/commands/importparts.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import os
+import logging
+
+from decimal import Decimal, InvalidOperation, ROUND_CEILING
+
+from django.db import DatabaseError
+from django.core.management.base import BaseCommand
+from django.contrib.contenttypes.models import ContentType
+
+from servo.models import Product, TaggedItem
+
+
+class Command(BaseCommand):
+
+ help = "Imports complete GSX parts database"
+
+ def handle(self, *args, **options):
+
+ update_prices = True
+ import_vintage = True
+ dbpath = "servo/uploads/products/partsdb.csv"
+
+ try:
+ partsdb = open(dbpath, "r")
+ except Exception:
+ pass
+
+ content_type = ContentType.objects.get(model="product")
+
+ for l in partsdb.readlines():
+
+ line = l.decode("iso-8859-1")
+ row = line.strip().split("\t")
+
+ if row[5] == "" or row[5] == "Currency":
+ continue # Skip header row and rows without currency
+
+ logging.debug(row)
+
+ category = row[0]
+
+ if re.match(r'~VIN', category) and not import_vintage:
+ continue # Skip vintage devices if so desired
+
+ p_number = row[1]
+
+ if re.match(r'675-', p_number):
+ continue # Skip DEPOT REPAIR INVOICE
+
+ p_title = row[2]
+ p_type = row[3]
+ lab_tier = row[4]
+
+ try:
+ stock_price = Decimal(row[6])
+ except InvalidOperation:
+ continue # Skip parts with no stock price
+
+ exchange_price = Decimal(row[7])
+
+ eee_code = row[8]
+
+ # skip substitute
+ component_group = row[10] or None
+ is_serialized = row[11]
+ req_diag = (row[12] == "Y")
+
+ product, created = Product.objects.get_or_create(code=p_number)
+
+ product.title = p_title
+ product.eee_code = eee_code
+ product.labour_tier = lab_tier
+ product.part_type = p_type or "OTHER"
+
+ product.component_code = component_group
+ product.is_serialized = (is_serialized == "Y")
+
+ if update_prices:
+ if stock_price:
+ purchase_sp = Decimal(stock_price)
+ product.price_purchase_stock = purchase_sp.to_integral_exact(rounding=ROUND_CEILING)
+ product.set_stock_sales_price()
+
+ if exchange_price:
+ purchase_ep = Decimal(exchange_price)
+ product.price_purchase_exchange = purchase_ep.to_integral_exact(rounding=ROUND_CEILING)
+ product.set_exchange_sales_price()
+
+ product.save()
+
+ try:
+ tag, created = TaggedItem.objects.get_or_create(
+ content_type=content_type,
+ object_id=product.pk,
+ tag=category)
+ tag.save()
+ except DatabaseError:
+ pass
+
+ os.unlink(dbpath)
diff --git a/servo/management/commands/migratepayments.py b/servo/management/commands/migratepayments.py
new file mode 100644
index 0000000..84c5c40
--- /dev/null
+++ b/servo/management/commands/migratepayments.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import logging
+
+from django.core.management.base import BaseCommand, CommandError
+
+from servo.models import Invoice, Payment
+
+
+class Command(BaseCommand):
+ help = "Migrate invoice payment info to Payments"
+
+ def handle(self, *args, **options):
+ Payment.objects.all().delete()
+ for i in Invoice.objects.all():
+ p = Payment(invoice=i, method=i.payment_method)
+ p.created_at = i.paid_at
+ p.created_by = i.created_by
+ p.amount = i.total_gross
+ p.save()
diff --git a/servo/management/commands/migratestatuses.py b/servo/management/commands/migratestatuses.py
new file mode 100644
index 0000000..e6ca601
--- /dev/null
+++ b/servo/management/commands/migratestatuses.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import logging
+
+from django.core.management.base import BaseCommand, CommandError
+
+from servo.models import Event, OrderStatus, Status
+
+
+class Command(BaseCommand):
+ help = "Migrate Events to Order Statuses"
+
+ def handle(self, *args, **options):
+ OrderStatus.objects.all().delete()
+
+ for i in Event.objects.filter(action='set_status'):
+ if i.content_object is None:
+ continue
+
+ os = OrderStatus(order=i.content_object)
+ os.started_by = i.triggered_by
+ os.started_at = i.triggered_at
+ os.finished_at = i.handled_at
+
+ try:
+ os.status = Status.objects.get(title=i.description)
+ except Status.DoesNotExist:
+ continue
+
+ os.save()
diff --git a/servo/management/commands/migratetimezones.py b/servo/management/commands/migratetimezones.py
new file mode 100644
index 0000000..7eb63d1
--- /dev/null
+++ b/servo/management/commands/migratetimezones.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import logging
+
+from django.core.management.base import BaseCommand, CommandError
+
+from servo.models import User
+
+
+class Command(BaseCommand):
+ help = "Migrate location timezones to user timezones"
+
+ def handle(self, *args, **options):
+ for i in User.objects.all():
+ if i.location:
+ i.timezone = i.location.timezone
+ i.save()
diff --git a/servo/management/commands/obfuscate.py b/servo/management/commands/obfuscate.py
new file mode 100644
index 0000000..8673846
--- /dev/null
+++ b/servo/management/commands/obfuscate.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from random import choice
+from django.db.utils import IntegrityError
+from django.template.defaultfilters import slugify
+from django.core.management.base import BaseCommand, CommandError
+
+from servo.models import Customer, Order, User, Location, GsxAccount
+
+
+class Command(BaseCommand):
+ def handle(self, *args, **options):
+ help = "Obfuscates the information in this Servo install"
+ names = ('Daniel Scott', 'Amy Collins', 'Linda Moore',
+ 'Dennis Parker', 'Mark Cox', 'Jesse Clark',
+ 'Brian Patterson', 'Andrew Bennett', 'Frank Lopez',
+ 'Benjamin Wood', 'Michelle Jenkins', 'Alice Lee',
+ 'Lois Gonzales', 'Diane Perez', 'Cheryl Torres',
+ 'Ernest Smith', 'Steve Mitchell', 'Barbara Jones',
+ 'Wanda Roberts', 'Julie Watson', 'Carlos Harris',
+ 'Anthony Phillips', 'Ralph Gray', 'Donna Hill',
+ 'Alan Coleman', 'Lawrence Ross', 'Stephen Flores',
+ 'Robert Simmons', 'Gloria White', 'Doris Wilson',
+ 'Shirley Sanders', 'Matthew Bell', 'Janice Hughes',
+ 'Walter Nelson', 'Gerald Taylor', 'Tammy Martin',
+ 'Gregory Barnes', 'Jonathan Baker', 'Lillian Green',
+ 'Brenda Hernandez', 'Denise Davis', 'Bobby Rogers',
+ 'Joe Lewis', 'Teresa Bailey', 'Craig Russell',
+ 'Angela Rivera', 'Rebecca Jackson', 'Nicole Henderson',
+ 'Kenneth James', 'Nicholas Bryant', 'Anne Washington',
+ 'Irene Miller', 'Theresa Martinez', 'Evelyn Sanchez',
+ 'Richard Anderson', 'Jeffrey Robinson', 'Heather Diaz',
+ 'Joshua Butler', 'Joan Peterson', 'Todd Campbell',
+ 'Timothy Kelly', 'Steven King', 'Norma Reed',
+ 'Carolyn Turner', 'Ruth Evans', 'Carol Thomas',
+ 'Arthur Howard', 'Peter Carter', 'Debra Ramirez',
+ 'Marie Walker', 'Donald Garcia', 'Janet Gonzalez',
+ 'Harold Adams', 'Bonnie Cook', 'Paula Long',
+ 'Bruce Griffin', 'Adam Hall' ,'Annie Young',
+ 'Jacqueline Alexander', 'Kimberly Edwards', 'Sarah Wright',
+ 'Terry Williams', 'Johnny Morris', 'Andrea Ward',
+ 'Margaret Allen', 'Sandra Price', 'Scott Foster',
+ 'Elizabeth Brown', 'Wayne Cooper', 'Mildred Brooks',
+ 'Dorothy Perry', 'Lori Powell', 'Kathryn Murphy',
+ 'Judy Johnson', 'Albert Morgan', 'William Richardson',
+ 'Randy Stewart', 'Roger Thompson', 'Anna Rodriguez',
+ )
+ """
+ print 'Munging customer names of open orders...'
+ for i in Order.objects.filter(state=Order.STATE_QUEUED):
+ if i.customer:
+ i.customer.name = choice(names)
+ i.customer.save()
+ """
+ print 'Munging technician names'
+ users = User.objects.exclude(username='filipp')
+ newnames = [x.split()[0].lower() for x in names]
+ oldnames = users.values_list("username", flat=True)
+ idx = 0
+
+ for i in users:
+ i.first_name, i.last_name = choice(names).split()
+ i.email = i.username + '@example.com'
+ i.save()
+
+ print 'Munging location names'
+ a = 65
+ for i in Location.objects.all():
+ #i.title = 'Location %s' % chr(a)
+ i.email = slugify(i.title) + '@example.com'
+ i.city = 'Cupertino'
+ i.phone = '0451 202 7' + str(a)
+ i.address = '1 Infinite Loop'
+ a += 1
+ i.save()
+
+ print 'Munging GSX account names'
+ a = 65
+ for i in GsxAccount.objects.all():
+ i.title = 'GSX Account %s' % chr(a)
+ a += 1
+ i.save()
+
diff --git a/servo/management/commands/slugifycategories.py b/servo/management/commands/slugifycategories.py
new file mode 100644
index 0000000..07ea3a1
--- /dev/null
+++ b/servo/management/commands/slugifycategories.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.utils.text import slugify
+from django.core.management.base import BaseCommand
+
+from servo.models import ProductCategory
+
+
+class Command(BaseCommand):
+ help = "Fixes ProductCategory slug fields"
+
+ def handle(self, *args, **options):
+ for i in ProductCategory.objects.all():
+ i.save()
diff --git a/servo/management/commands/sutostaff.py b/servo/management/commands/sutostaff.py
new file mode 100644
index 0000000..6450603
--- /dev/null
+++ b/servo/management/commands/sutostaff.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import logging
+from email.parser import Parser
+
+from django.core.management.base import BaseCommand, CommandError
+
+from servo.models import User
+
+
+class Command(BaseCommand):
+ help = "Converts SuperUsers to Staff"
+
+ def handle(self, *args, **options):
+ for u in User.objects.filter(is_superuser=True):
+ u.is_superuser = False
+ u.is_staff = True
+ u.save()
+ \ No newline at end of file
diff --git a/servo/management/commands/tokenize.py b/servo/management/commands/tokenize.py
new file mode 100644
index 0000000..fd5daa9
--- /dev/null
+++ b/servo/management/commands/tokenize.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.db.models import Q
+from django.utils import timezone
+from django.core.management.base import BaseCommand, CommandError
+
+from servo.models import User
+
+
+class Command(BaseCommand):
+ help = "Creates API token for user"
+
+ def handle(self, *args, **options):
+ user = User.objects.get(username=args[0])
+ print(user.create_token())
diff --git a/servo/management/commands/updatecomponents.py b/servo/management/commands/updatecomponents.py
new file mode 100644
index 0000000..75b6de5
--- /dev/null
+++ b/servo/management/commands/updatecomponents.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+
+from servo.models import User, GsxAccount, Product, ServicePart
+
+
+class Command(BaseCommand):
+ help = "Update missing component codes"
+
+ def handle(self, *args, **options):
+ u = User.objects.get(username='filipp')
+ GsxAccount.default(u)
+ for p in Product.objects.filter(component_code='').exclude(labour_tier=''):
+ try:
+ info = ServicePart(part_number=p.code).lookup()
+ p.component_code = info.componentCode.strip()
+ p.save()
+ except Exception, e:
+ print e
diff --git a/servo/management/commands/updateorders.py b/servo/management/commands/updateorders.py
new file mode 100644
index 0000000..caad815
--- /dev/null
+++ b/servo/management/commands/updateorders.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+
+from servo.models import Order
+
+
+class Command(BaseCommand):
+
+ help = "Updates order descriptions"
+
+ def handle(self, *args, **options):
+ for o in Order.objects.filter(description=""):
+ o.description = o.device_name() or ""
+ o.save()
+
+ for o in Order.objects.filter(status_name=""):
+ o.status_name = o.get_status_name() or ""
+ o.save()
+
+ for o in Order.objects.filter(customer_name=""):
+ o.customer_name = o.get_customer_name() or ""
+ o.save()
diff --git a/servo/management/commands/updatepototals.py b/servo/management/commands/updatepototals.py
new file mode 100644
index 0000000..ae8f7cd
--- /dev/null
+++ b/servo/management/commands/updatepototals.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+from servo.models import PurchaseOrder
+
+
+class Command(BaseCommand):
+
+ help = "Update Purchase Order totals"
+
+ def handle(self, *args, **options):
+ for po in PurchaseOrder.objects.all():
+ po.total = po.sum()
+ po.save()
diff --git a/servo/management/commands/updateprices.py b/servo/management/commands/updateprices.py
new file mode 100644
index 0000000..a916870
--- /dev/null
+++ b/servo/management/commands/updateprices.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import sys
+import logging
+
+from django.core.management.base import BaseCommand
+from servo.models import (Product, GsxAccount, User,)
+
+
+class Command(BaseCommand):
+ help = "Updates all part prices from GSX"
+
+ def handle(self, *args, **options):
+ if len(args) < 1:
+ print "Usage: updateprices username [start:finish]"
+ sys.exit(1)
+
+ start, counter = 0, 0
+ finish = 999999999999
+
+ if len(args) == 2:
+ start, finish = args[1].split(':')
+
+ GsxAccount.default(User.objects.get(username=args[0]))
+
+ products = Product.objects.filter(pk__gt=start, pk__lt=finish)
+ products = products.exclude(part_type='SERVICE')
+ products = products.exclude(fixed_price=True)
+
+ for i in products.order_by('id'):
+ logging.debug('Updating product %d' % i.pk)
+ try:
+ i.update_price()
+ i.save()
+ counter += 1
+ except Exception, e:
+ logging.debug(e)
+
+ print '%d product prices updated' % counter
diff --git a/servo/management/commands/updaterepairs.py b/servo/management/commands/updaterepairs.py
new file mode 100644
index 0000000..4eb1b58
--- /dev/null
+++ b/servo/management/commands/updaterepairs.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.core.management.base import BaseCommand
+
+from servo.models import Repair
+
+
+class Command(BaseCommand):
+
+ help = "Updates statuses and details of open GSX repairs"
+
+ def handle(self, *args, **options):
+ repairs = Repair.objects.filter(completed_at=None)
+
+ for r in repairs.exclude(confirmation=""):
+ r.connect_gsx()
+ try:
+ details = r.get_details()
+ r.update_details(details)
+ except Exception:
+ pass
diff --git a/servo/messaging/__init__.py b/servo/messaging/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/messaging/__init__.py
diff --git a/servo/messaging/sms.py b/servo/messaging/sms.py
new file mode 100644
index 0000000..a51db7a
--- /dev/null
+++ b/servo/messaging/sms.py
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import urllib
+from hashlib import md5
+from django.conf import settings
+from django.utils.translation import ugettext as _
+
+from servo.models.common import Configuration
+
+class BaseSMSProvider(object):
+ def __init__(self, recipient, note, msg):
+ self.conf = Configuration.conf()
+
+ if not self.conf.get('sms_http_sender'):
+ raise ValueError(_("SMS sender not configured"))
+
+ if len(recipient) < 8:
+ recipient = '372' + recipient
+
+ recipient = re.sub(r'[\+\s\-]', '', recipient)
+ self.recipient = recipient.lstrip('+')
+ self.note = note
+ self.msg = msg
+
+ self.sender = self.conf['sms_http_sender']
+ self.body = self.note.body.encode('utf-8')
+
+
+class SMSJazzProvider(object):
+ """
+ SMS Jazz gateway provider
+ Used mainly by Estonian AASPs, hence default prefix of 372
+ """
+ URL = 'https://www.smsjazz.net/SMSjazzAPI/sendsms.php'
+
+ STATUSES = {
+ '1' : ('DELIVERED', 'SMS message is delivered to mobile phone'),
+ '2' : ('FAILED', 'SMS message has failed and is not delivered to mobile phone'),
+ '4' : ('SENT', 'SMS message is buffered and is waiting to be delivered to mobile phone'),
+ '8' : ('SENT', 'SMS message is buffered and is waiting to be delivered to mobile phone'),
+ '16': ('FAILED', 'Error in sender/receiver/message parameters'),
+ '32': ('SENT', ''),
+ }
+
+ def __init__(self, recipient, note, msg):
+ if len(recipient) < 8:
+ recipient = '372' + recipient
+
+ recipient = re.sub(r'[\+\s\-]', '', recipient)
+ self.recipient = recipient.lstrip('+')
+ self.note = note
+ self.msg = msg
+
+ def send(self):
+ """
+ Sends SMS through SMS Jazz Gateway
+ """
+ conf = Configuration.conf()
+
+ if not conf.get('sms_http_sender'):
+ raise ValueError(_("SMS sender name not configured"))
+
+ body = self.note.body.encode('utf-8')
+ sender = conf.get('sms_http_sender')
+ pwhash = md5(conf['sms_http_password']).hexdigest()
+ checksum = md5(body + self.recipient.encode('ascii') + pwhash).hexdigest()
+
+ params = {
+ 'username' : conf['sms_http_user'],
+ 'password' : pwhash,
+ 'message' : body,
+ 'sender' : sender.encode('ascii', 'replace'),
+ 'receiver' : self.recipient,
+ 'charset' : 'UTF8',
+ 'checksum' : checksum,
+ }
+
+ if self.msg:
+ dlruri = '/api/messages/?id={0}&status=%status%'.format(self.msg.code)
+ dlruri = settings.SERVO_URL + dlruri
+ params['dlruri'] = dlruri
+
+ params = urllib.urlencode(params)
+ from ssl import _create_unverified_context
+ r = urllib.urlopen(self.URL, params, context=_create_unverified_context()).read()
+
+ if not '1:OK' in r:
+ raise ValueError(_('Failed to send message to %s') % self.recipient)
+
+
+class HQSMSProvider(BaseSMSProvider):
+ """
+ HQSMS Gateway Provider.
+ API docs: http://www.hqsms.com/media/page/docs/HQSMS_https.pdf
+ """
+ URL = 'https://ssl.hqsms.com/sms.do'
+ BACKUP_URL = 'https://ssl2.hqsms.com/sms.do'
+
+ ERRORS = {
+ "ERROR:13" : _("Lack of valid phone numbers (invalid or blacklisted numbers)"),
+ "ERROR:14" : _("Wrong sender name"),
+ "ERROR:19" : _("Too many messages in one request"),
+ "ERROR:102" : _("Invalid username or password"),
+ "ERROR:103" : _("Insufficient credits on your account"),
+ "ERROR:200" : _("Unsuccessful message submission"),
+ "ERROR:201" : _("Internal system error"),
+ "ERROR:999" : _("Internal system error"),
+ }
+
+ STATUSES = {
+ '401' : ('NOT_FOUND', 'Wrong ID or report has expired'),
+ '402' : ('EXPIRED', 'Messages expired'),
+ '403' : ('SENT', 'Message is sent'),
+ '404' : ('DELIVERED', 'Message is delivered to recipient'),
+ '405' : ('UNDELIVERED', 'Message is undelivered (invalid number, roaming error etc)'),
+ '406' : ('FAILED', 'Sending message failed – please report it to us'),
+ '407' : ('REJECTED', 'Message is undelivered (invalid number, roaming error etc)'),
+ '408' : ('UNKNOWN', 'No report (message may be either delivered or not)'),
+ '409' : ('QUEUED', 'Message is waiting to be sent'),
+ '410' : ('ACCEPTED', 'Message is delivered to operator'),
+ }
+
+ def send(self):
+ pwhash = md5(self.conf['sms_http_password']).hexdigest()
+
+ params = {
+ 'username' : self.conf['sms_http_user'],
+ 'password' : pwhash,
+ 'message' : self.body,
+ 'from' : self.sender,
+ 'to' : self.recipient,
+ }
+
+ if self.msg:
+ dlruri = settings.SERVO_URL + '/api/messages/?id={0}'.format(self.msg.code)
+ params['notify_url'] = dlruri
+
+ params = urllib.urlencode(params)
+ from ssl import _create_unverified_context
+ r = urllib.urlopen(self.URL, params, context=_create_unverified_context()).read()
+
+ if 'ERROR:' in r:
+ raise ValueError(self.ERRORS.get(r, _('Unknown error (%s)') % r))
+
+ def set_status(self, msg):
+ pass
+
+
+class HttpProvider(object):
+ """
+ Sends SMS through a HTTP gateway (ie Kannel)
+ """
+ def send(self, note, number):
+ conf = Configuration.conf()
+
+ if not conf.get('sms_http_url'):
+ raise ValueError(_("No SMS HTTP gateway defined"))
+
+ params = urllib.urlencode({
+ 'username' : conf['sms_http_user'],
+ 'password' : conf['sms_http_password'],
+ 'text' : note.body.encode('utf8'),
+ 'to' : number
+ })
+
+ from ssl import _create_unverified_context
+ f = urllib.urlopen("%s?%s" % (conf['sms_http_url'], params), context=_create_unverified_context())
+ return f.read()
diff --git a/servo/migrations/0001_initial.py b/servo/migrations/0001_initial.py
new file mode 100644
index 0000000..b302c71
--- /dev/null
+++ b/servo/migrations/0001_initial.py
@@ -0,0 +1,1373 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import mptt.fields
+import servo.lib.shorturl
+import servo.models.product
+import django.utils.timezone
+import servo.defaults
+import django.db.models.deletion
+from django.conf import settings
+import django.core.validators
+import servo.validators
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('auth', '0001_initial'),
+ ('sites', '0001_initial'),
+ ('contenttypes', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='User',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ ('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, max_length=30, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')])),
+ ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
+ ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
+ ('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ('full_name', models.CharField(default='New User', max_length=128, editable=False)),
+ ('locale', models.CharField(default=b'da_DK.UTF-8', help_text='Select which language you want to use Servo in.', max_length=32, verbose_name='language', choices=[(b'da_DK.UTF-8', 'Danish'), (b'nl_NL.UTF-8', 'Dutch'), (b'en_US.UTF-8', 'English'), (b'et_EE.UTF-8', 'Estonian'), (b'fi_FI.UTF-8', 'Finnish'), (b'sv_SE.UTF-8', 'Swedish')])),
+ ('timezone', models.CharField(default=b'UTC', max_length=128, verbose_name='Time zone', choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montreal', b'America/Montreal'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Santa_Isabel', b'America/Santa_Isabel'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Rangoon', b'Asia/Rangoon'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GMT', b'GMT'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Johnston', b'Pacific/Johnston'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'US/Alaska', b'US/Alaska'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'UTC', b'UTC')])),
+ ('region', models.CharField(default=servo.defaults.locale, help_text='Affects formatting of numbers, dates and currencies.', max_length=32, verbose_name='region', choices=[(b'da_DK.UTF-8', 'Denmark'), (b'et_EE.UTF-8', 'Estonia'), (b'fi_FI.UTF-8', 'Finland'), (b'en_US.UTF-8', 'United States'), (b'nl_NL.UTF-8', 'Netherlands'), (b'sv_SE.UTF-8', 'Sweden')])),
+ ('notify_by_email', models.BooleanField(default=False, help_text='Event notifications will also be emailed to you.', verbose_name='email notifications')),
+ ('autoprint', models.BooleanField(default=True, help_text='Opens print dialog automatically.', verbose_name='print automatically')),
+ ('tech_id', models.CharField(default=b'', max_length=16, verbose_name='tech ID', blank=True)),
+ ('gsx_userid', models.CharField(default=b'', max_length=128, verbose_name='User ID', blank=True)),
+ ('gsx_password', models.CharField(default=b'', max_length=256, verbose_name='Password', blank=True)),
+ ('gsx_poprefix', models.CharField(default=b'', help_text='GSX repairs you create will be prefixed', max_length=8, verbose_name='PO prefix', blank=True)),
+ ('photo', models.ImageField(help_text='Maximum avatar size is 1MB', upload_to=b'avatars', null=True, verbose_name='photo', blank=True)),
+ ('is_visible', models.BooleanField(default=True, editable=False)),
+ ],
+ options={
+ 'ordering': ('full_name',),
+ 'verbose_name': 'User',
+ 'verbose_name_plural': 'Users & Groups',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Accessory',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=255)),
+ ('qty', models.IntegerField(default=1)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Attachment',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('mime_type', models.CharField(max_length=64, editable=False)),
+ ('content', models.FileField(upload_to=b'attachments', verbose_name='file', validators=[servo.validators.file_upload_validator])),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Calendar',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Calendar', max_length=128, verbose_name='title')),
+ ('hours_per_day', models.FloatField(help_text='How many hours per day should be in this calendar', null=True, verbose_name='hours per day', blank=True)),
+ ('user', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='CalendarEvent',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('started_at', models.DateTimeField(default=django.utils.timezone.now)),
+ ('finished_at', models.DateTimeField(null=True, blank=True)),
+ ('seconds', models.PositiveIntegerField(null=True, editable=False)),
+ ('notes', models.TextField(null=True, blank=True)),
+ ('calendar', models.ForeignKey(editable=False, to='servo.Calendar')),
+ ],
+ options={
+ 'ordering': ['-started_at'],
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Checklist',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Checklist', unique=True, max_length=255, verbose_name='title')),
+ ('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
+ ],
+ options={
+ 'ordering': ('title',),
+ 'verbose_name': 'Checklist',
+ 'verbose_name_plural': 'Checklists',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ChecklistItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(max_length=255, verbose_name='Task')),
+ ('description', models.TextField(default=b'', verbose_name='Description', blank=True)),
+ ('checklist', models.ForeignKey(to='servo.Checklist')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ChecklistItemValue',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('checked_at', models.DateTimeField(auto_now_add=True)),
+ ('checked_by', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+ ('item', models.ForeignKey(to='servo.ChecklistItem')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Configuration',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('key', models.CharField(max_length=255)),
+ ('value', models.TextField(default=b'', blank=True)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ContactInfo',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('key', models.CharField(max_length=255)),
+ ('value', models.CharField(max_length=255)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Customer',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(default='New Customer', max_length=255, verbose_name='name')),
+ ('fullname', models.CharField(default=b'', max_length=255, editable=False)),
+ ('phone', models.CharField(default=b'', max_length=32, verbose_name='phone', blank=True)),
+ ('email', models.EmailField(default=b'', max_length=75, verbose_name='email', blank=True)),
+ ('street_address', models.CharField(default=b'', max_length=128, verbose_name='address', blank=True)),
+ ('zip_code', models.CharField(default=b'', max_length=32, verbose_name='ZIP Code', blank=True)),
+ ('city', models.CharField(default=b'', max_length=32, verbose_name='city', blank=True)),
+ ('country', models.CharField(default=servo.defaults.country, max_length=2, verbose_name='Country', blank=True, choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'Samoa (American)'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AX', 'Aaland Islands'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia & Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BL', 'St Barthelemy'), ('BM', 'Bermuda'), ('BN', 'Brunei'), ('BO', 'Bolivia'), ('BQ', 'Caribbean Netherlands'), ('BR', 'Brazil'), ('BS', 'Bahamas'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CD', 'Congo (Dem. Rep.)'), ('CF', 'Central African Rep.'), ('CG', 'Congo (Rep.)'), ('CH', 'Switzerland'), ('CI', "Cote d'Ivoire"), ('CK', 'Cook Islands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CW', 'Curacao'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('GA', 'Gabon'), ('GB', 'Britain (UK)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GG', 'Guernsey'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia & the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard Island & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IM', 'Isle of Man'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JE', 'Jersey'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St Kitts & Nevis'), ('KP', 'Korea (North)'), ('KR', 'Korea (South)'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', 'Laos'), ('LB', 'Lebanon'), ('LC', 'St Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova'), ('ME', 'Montenegro'), ('MF', 'St Martin (French part)'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('MK', 'Macedonia'), ('ML', 'Mali'), ('MM', 'Myanmar (Burma)'), ('MN', 'Mongolia'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Montserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PS', 'Palestine'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RS', 'Serbia'), ('RU', 'Russia'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('SS', 'South Sudan'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SX', 'St Maarten (Dutch part)'), ('SY', 'Syria'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Is'), ('TD', 'Chad'), ('TF', 'French Southern & Antarctic Lands'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TL', 'East Timor'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan'), ('TZ', 'Tanzania'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'US minor outlying islands'), ('US', 'United States'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City'), ('VC', 'St Vincent'), ('VE', 'Venezuela'), ('VG', 'Virgin Islands (UK)'), ('VI', 'Virgin Islands (US)'), ('VN', 'Vietnam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna'), ('WS', 'Samoa (western)'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')])),
+ ('photo', models.ImageField(upload_to=b'photos', null=True, verbose_name='photo', blank=True)),
+ ('notes', models.TextField(default=b'', verbose_name='notes', blank=True)),
+ ('created_at', models.DateTimeField(auto_now=True)),
+ ('is_company', models.BooleanField(default=False, help_text='companies can contain other contacts', verbose_name='company')),
+ ('lft', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('level', models.PositiveIntegerField(editable=False, db_index=True)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='CustomerGroup',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(default='New Group', unique=True, max_length=255, verbose_name='name')),
+ ('slug', models.SlugField(editable=False)),
+ ],
+ options={
+ 'ordering': ('id',),
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Device',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('sn', models.CharField(default=b'', max_length=32, verbose_name='Serial Number', blank=True, validators=[servo.validators.sn_validator])),
+ ('description', models.CharField(default='New Device', max_length=128, verbose_name='description')),
+ ('brand', models.CharField(default='Apple', max_length=128, verbose_name='Brand', blank=True)),
+ ('reseller', models.CharField(default=b'', max_length=128, verbose_name='Reseller', blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True, null=True)),
+ ('imei', models.CharField(default=b'', max_length=15, verbose_name='IMEI Number', blank=True)),
+ ('initial_activation_policy', models.CharField(default=b'', verbose_name='Initial Activation Policy', max_length=128, editable=False)),
+ ('applied_activation_policy', models.CharField(default=b'', verbose_name='Applied Activation Policy', max_length=128, editable=False)),
+ ('unlocked', models.NullBooleanField(default=None, editable=False)),
+ ('slug', models.SlugField(max_length=128, null=True, editable=False)),
+ ('product_line', models.CharField(default=b'OTHER', max_length=16, verbose_name='Product Line', choices=[(b'IPODCLASSIC', b'iPod Classic'), (b'POWERMAC', b'Power Mac'), (b'APPLETV', b'Apple TV'), (b'IMAC', b'iMac'), (b'OTHER', b'Other Products'), (b'MACBOOKAIR', b'MacBook Air'), (b'DISPLAYS', b'Display'), (b'IPODTOUCH', b'iPod Touch'), (b'MACPRO', b'Mac Pro'), (b'IPODNANO', b'iPod nano'), (b'IPAD', b'iPad'), (b'MACBOOK', b'MacBook'), (b'MACACCESSORY', b'Mac Accessory'), (b'MACMINI', b'Mac mini'), (b'SERVER', b'Server'), (b'IPHONE', b'iPhone'), (b'IPHONEACCESSORY', b'iPhone Accessory'), (b'IPODSHUFFLE', b'iPod Shuffle'), (b'MACBOOKPRO', b'MacBook Pro')])),
+ ('config_code', models.CharField(default=b'', max_length=8, editable=False)),
+ ('configuration', models.CharField(default=b'', max_length=256, verbose_name='configuration', blank=True)),
+ ('warranty_status', models.CharField(default=b'NA', max_length=3, verbose_name='Warranty Status', choices=[(b'QP', 'Quality Program'), (b'CS', 'Customer Satisfaction'), (b'ALW', 'Apple Limited Warranty'), (b'APP', 'AppleCare Protection Plan'), (b'CC', 'Custom Bid Contracts'), (b'WTY', "3'rd Party Warranty"), (b'OOW', 'Out Of Warranty (No Coverage)'), (b'NA', 'Unknown')])),
+ ('username', models.CharField(default=b'', max_length=32, verbose_name='username', blank=True)),
+ ('password', models.CharField(default=b'', max_length=32, verbose_name='password', blank=True)),
+ ('purchased_on', models.DateField(null=True, verbose_name='Date Purchased', blank=True)),
+ ('purchase_country', models.CharField(default=servo.defaults.country, editable=False, choices=[('AF', 'Afghanistan'), ('AX', '\xc5land Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('CV', 'Cabo Verde'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo (the Democratic Republic of the)'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "C\xf4te d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CW', 'Cura\xe7ao'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands [Malvinas]'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia (The)'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See [Vatican City State]'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', 'Laos'), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia (the Federated States of)'), ('MD', 'Moldovia'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('KP', 'North Korea'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestine, State of'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'R\xe9union'), ('RO', 'Romania'), ('RU', 'Russia'), ('RW', 'Rwanda'), ('BL', 'Saint Barth\xe9lemy'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin (French part)'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SX', 'Sint Maarten (Dutch part)'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('KR', 'South Korea'), ('SS', 'South Sudan'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syria'), ('TW', 'Taiwan'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('GB', 'United Kingdom'), ('US', 'United States'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Vietnam'), ('VG', 'Virgin Islands (British)'), ('VI', 'Virgin Islands (U.S.)'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], max_length=128, blank=True, verbose_name='Purchase Country')),
+ ('sla_description', models.TextField(null=True, editable=False)),
+ ('has_onsite', models.BooleanField(default=False, help_text='Device is eligible for onsite repairs in GSX')),
+ ('contract_start_date', models.DateField(null=True, editable=False)),
+ ('contract_end_date', models.DateField(null=True, editable=False)),
+ ('onsite_start_date', models.DateField(null=True, editable=False)),
+ ('onsite_end_date', models.DateField(null=True, editable=False)),
+ ('parts_and_labor_covered', models.BooleanField(default=False, editable=False)),
+ ('notes', models.TextField(default=b'', verbose_name='notes', blank=True)),
+ ('photo', models.ImageField(upload_to=b'devices', null=True, verbose_name='photo', blank=True)),
+ ('image_url', models.URLField(null=True, verbose_name='Image URL', blank=True)),
+ ('manual_url', models.URLField(null=True, verbose_name='Manual URL', blank=True)),
+ ('exploded_view_url', models.URLField(null=True, verbose_name='Exploded View', blank=True)),
+ ('is_vintage', models.BooleanField(default=False, help_text='Device is considered vintage in GSX', verbose_name=b'vintage')),
+ ('fmip_active', models.BooleanField(default=False, editable=False)),
+ ],
+ options={
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='DeviceGroup',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(unique=True, max_length=128)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Escalation',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('escalation_id', models.CharField(default=b'', max_length=22, editable=False)),
+ ('contexts', models.TextField(default=b'{}', blank=True)),
+ ('issue_type', models.CharField(default=b'', max_length=4, blank=True, choices=[(b'AMQ', b'Account Management Question'), (b'UQ', b'GSX Usage Question'), (b'OSI', b'Order Status Issue'), (b'PRI', b'Part Return Issue'), (b'PPOR', b'Problem Placing Order/Repair'), (b'PUR', b'Problem Updating Repair'), (b'SCI', b'Shipping Carrier Issue'), (b'SES', b'Service Excellence Scoring'), (b'ARF', b'Apple Retail Feedback'), (b'DF', b'Depot Feedback'), (b'FS', b'GSX Feedback/Suggestion'), (b'WS', b'GSX Web Services (API)'), (b'SEPI', b'Service Excellence Program Information'), (b'TTI', b'Technical or Troubleshooting Issue'), (b'DTA', b'Diagnostic Tool Assistance'), (b'BIQ', b'Billing or Invoice Question'), (b'SESC', b'Safety Issue')])),
+ ('status', models.CharField(default=b'O', max_length=1, choices=[(b'O', b'Open'), (b'C', b'Closed'), (b'E', b'Escalated')])),
+ ('submitted_at', models.DateTimeField(null=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('created_by', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Event',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('description', models.CharField(max_length=255)),
+ ('triggered_at', models.DateTimeField(auto_now_add=True)),
+ ('handled_at', models.DateTimeField(null=True)),
+ ('action', models.CharField(max_length=32)),
+ ('priority', models.SmallIntegerField(default=1)),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+ ('notify_users', models.ManyToManyField(related_name='notifications', to=settings.AUTH_USER_MODEL)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ('triggered_by', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('priority', '-id'),
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='FlaggedItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+ ('flagged_by', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='GsxAccount',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New GSX Account', max_length=128)),
+ ('sold_to', models.CharField(max_length=10, verbose_name='Sold-To')),
+ ('ship_to', models.CharField(max_length=10, verbose_name='Ship-To')),
+ ('region', models.CharField(max_length=3, verbose_name='Region', choices=[(b'002', b'Asia/Pacific'), (b'003', b'Japan'), (b'004', b'Europe'), (b'005', b'United States'), (b'006', b'Canadia'), (b'007', b'Latin America')])),
+ ('user_id', models.CharField(default=b'', max_length=128, verbose_name='User ID', blank=True)),
+ ('password', models.CharField(default=b'', max_length=256, verbose_name='Password', blank=True)),
+ ('environment', models.CharField(default=b'pr', max_length=2, verbose_name='Environment', choices=[(b'pr', b'Production'), (b'ut', b'Development'), (b'it', b'Testing')])),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ['title'],
+ 'get_latest_by': 'id',
+ 'verbose_name': 'GSX Account',
+ 'verbose_name_plural': 'GSX Accounts',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Inventory',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('amount_minimum', models.PositiveIntegerField(default=0, verbose_name='minimum amount')),
+ ('amount_reserved', models.PositiveIntegerField(default=0, verbose_name='reserved amount')),
+ ('amount_stocked', models.IntegerField(default=0, verbose_name='stocked amount')),
+ ('amount_ordered', models.PositiveIntegerField(default=0, verbose_name='ordered amount')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Invoice',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('payment_method', models.IntegerField(default=0, verbose_name='Payment Method', editable=False, choices=[(0, 'No Charge'), (1, 'Cash'), (2, 'Invoice'), (3, 'Credit Card'), (4, 'Mail payment'), (5, 'Online payment')])),
+ ('is_paid', models.BooleanField(default=False, verbose_name='paid')),
+ ('paid_at', models.DateTimeField(null=True, editable=False)),
+ ('customer_name', models.CharField(default='Walk-in', max_length=255, verbose_name='Name')),
+ ('customer_phone', models.CharField(max_length=128, null=True, verbose_name='Phone', blank=True)),
+ ('customer_email', models.CharField(max_length=128, null=True, verbose_name='Email', blank=True)),
+ ('customer_address', models.CharField(max_length=255, null=True, verbose_name='Address', blank=True)),
+ ('reference', models.CharField(max_length=255, null=True, verbose_name='Reference', blank=True)),
+ ('total_net', models.DecimalField(max_digits=8, decimal_places=2)),
+ ('total_tax', models.DecimalField(max_digits=8, decimal_places=2)),
+ ('total_gross', models.DecimalField(max_digits=8, decimal_places=2)),
+ ('total_margin', models.DecimalField(editable=False, max_digits=8, decimal_places=2)),
+ ('created_by', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL)),
+ ('customer', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, editable=False, to='servo.Customer', null=True)),
+ ],
+ options={
+ 'ordering': ('-id',),
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='InvoiceItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('code', models.CharField(default=b'', max_length=128, blank=True)),
+ ('title', models.CharField(max_length=128, verbose_name='title')),
+ ('description', models.TextField(default=b'', verbose_name='description', blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('amount', models.IntegerField(default=1, verbose_name='amount')),
+ ('sn', models.CharField(default=b'', max_length=32, verbose_name='KGB Serial Number', blank=True)),
+ ('price', models.DecimalField(verbose_name='Sales Price', max_digits=8, decimal_places=2)),
+ ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ('invoice', models.ForeignKey(to='servo.Invoice')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Location',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Location', max_length=255, verbose_name='name')),
+ ('phone', models.CharField(default=b'', max_length=32, verbose_name='phone', blank=True)),
+ ('email', models.EmailField(default=b'', max_length=75, verbose_name='email', blank=True)),
+ ('address', models.CharField(default=b'', max_length=32, verbose_name='address', blank=True)),
+ ('zip_code', models.CharField(default=b'', max_length=8, verbose_name='ZIP Code', blank=True)),
+ ('city', models.CharField(default=b'', max_length=16, verbose_name='city', blank=True)),
+ ('timezone', models.CharField(default=b'UTC', max_length=128, verbose_name='Time zone', choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montreal', b'America/Montreal'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Santa_Isabel', b'America/Santa_Isabel'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Rangoon', b'Asia/Rangoon'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GMT', b'GMT'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Johnston', b'Pacific/Johnston'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'US/Alaska', b'US/Alaska'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'UTC', b'UTC')])),
+ ('gsx_shipto', models.CharField(default=b'', max_length=10, verbose_name='Ship-To', blank=True)),
+ ('gsx_tz', models.CharField(default=b'CEST', max_length=4, verbose_name='Timezone', choices=[(b'PST', b'UTC - 8h (Pacific Standard Time)'), (b'PDT', b'UTC - 7h (Pacific Daylight Time)'), (b'CST', b'UTC - 6h (Central Standard Time)'), (b'CDT', b'UTC - 5h (Central Daylight Time)'), (b'EST', b'UTC - 5h (Eastern Standard Time)'), (b'EDT', b'UTC - 4h (Eastern Daylight Time)'), (b'GMT', b'UTC (Greenwich Mean Time)'), (b'CET', b'UTC + 1h (Central European Time)'), (b'CEST', b'UTC + 2h (Central European Summer Time)'), (b'USZ1', b'UTC + 3h (Kaliningrad Time)'), (b'MSK', b'UTC + 4h (Moscow Time)'), (b'IST', b'UTC + 5.5h (Indian Standard Time)'), (b'YEKST', b'UTC + 6h (Yekaterinburg Time)'), (b'OMSST', b'UTC + 7h (Omsk Time)'), (b'KRAST', b'UTC + 8h (Krasnoyarsk Time)'), (b'CCT', b'UTC + 8h (Chinese Coast Time)'), (b'IRKST', b'UTC + 9h (Irkutsk Time)'), (b'JST', b'UTC + 9h (Japan Standard Time)'), (b'YAKST', b'UTC + 10h (Yakutsk Time)'), (b'AEST', b'UTC + 10h (Australian Eastern Standard Time)'), (b'VLAST', b'UTC + 11h (Vladivostok Time)'), (b'AEDT', b'UTC + 11h (Australian Eastern Daylight Time)'), (b'ACST', b'UTC + 9.5h (Austrailian Central Standard Time)'), (b'ACDT', b'UTC + 10.5h (Australian Central Daylight Time)'), (b'NZST', b'UTC + 12h (New Zealand Standard Time)'), (b'MAGST', b'UTC + 12h (Magadan Time)')])),
+ ('notes', models.TextField(default=b'9:00 - 18:00', help_text='Will be shown on print templates', verbose_name='Notes', blank=True)),
+ ('logo', models.FileField(upload_to=b'logos', null=True, verbose_name='Logo', blank=True)),
+ ('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
+ ('gsx_accounts', models.ManyToManyField(to='servo.GsxAccount', null=True, verbose_name='Accounts', blank=True)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ('title',),
+ 'get_latest_by': 'id',
+ 'verbose_name': 'Location',
+ 'verbose_name_plural': 'Locations',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Message',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('code', models.CharField(default=servo.defaults.uuid, unique=True, max_length=36)),
+ ('sender', models.CharField(max_length=128)),
+ ('recipient', models.CharField(max_length=128)),
+ ('body', models.TextField()),
+ ('sent_at', models.DateTimeField(null=True)),
+ ('received_at', models.DateTimeField(null=True)),
+ ('status', models.CharField(max_length=16, choices=[(b'SENT', b'SENT'), (b'DELIVERED', b'DELIVERED'), (b'RECEIVED', b'RECEIVED'), (b'FAILED', b'FAILED')])),
+ ('method', models.CharField(default=b'EMAIL', max_length=16, choices=[(b'EMAIL', b'EMAIL'), (b'SMS', b'SMS'), (b'GSX', b'GSX')])),
+ ('error', models.TextField()),
+ ('created_by', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Note',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('subject', models.CharField(default=servo.defaults.subject, max_length=255, verbose_name='subject', blank=True)),
+ ('body', models.TextField(verbose_name='Message')),
+ ('code', models.CharField(default=servo.lib.shorturl.from_time, unique=True, max_length=9, editable=False)),
+ ('sender', models.CharField(default=b'', max_length=255, verbose_name='From')),
+ ('recipient', models.CharField(default=b'', max_length=255, verbose_name='To', blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('sent_at', models.DateTimeField(null=True, editable=False)),
+ ('is_reported', models.BooleanField(default=False, verbose_name='report')),
+ ('is_read', models.BooleanField(default=True, verbose_name='read', editable=False)),
+ ('is_flagged', models.BooleanField(default=False, verbose_name='flagged', editable=False)),
+ ('lft', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('level', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('created_by', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL)),
+ ('customer', models.ForeignKey(blank=True, to='servo.Customer', null=True)),
+ ('escalation', models.ForeignKey(editable=False, to='servo.Escalation', null=True)),
+ ],
+ options={
+ 'get_latest_by': 'created_at',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('kind', models.CharField(max_length=16)),
+ ('action', models.CharField(max_length=16)),
+ ('message', models.TextField()),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Order',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('code', models.CharField(max_length=8, unique=True, null=True)),
+ ('url_code', models.CharField(max_length=8, unique=True, null=True)),
+ ('description', models.CharField(default=b'', max_length=128)),
+ ('status_icon', models.CharField(default=b'undefined', max_length=16)),
+ ('priority', models.IntegerField(default=1, verbose_name='priority', choices=[(2, 'High'), (1, 'Normal'), (0, 'Low')])),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('started_at', models.DateTimeField(null=True)),
+ ('closed_at', models.DateTimeField(null=True)),
+ ('place', models.CharField(default=b'', max_length=128)),
+ ('customer_name', models.CharField(default=b'', max_length=128)),
+ ('state', models.IntegerField(default=0, max_length=1, choices=[(0, 'Unassigned'), (1, 'Open'), (2, 'Closed')])),
+ ('status_name', models.CharField(default=b'', max_length=128)),
+ ('status_started_at', models.DateTimeField(null=True)),
+ ('status_limit_green', models.DateTimeField(null=True)),
+ ('status_limit_yellow', models.DateTimeField(null=True)),
+ ('checkin_location', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to='servo.Location', null=True)),
+ ('checkout_location', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to='servo.Location', null=True)),
+ ('closed_by', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
+ ('created_by', models.ForeignKey(related_name='created_orders', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
+ ('customer', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to='servo.Customer', null=True)),
+ ],
+ options={
+ 'ordering': ('-priority', 'id'),
+ 'permissions': (('change_user', 'Can set assignee'), ('change_status', 'Can change status'), ('follow_order', 'Can follow order')),
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='OrderDevice',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('should_report', models.BooleanField(default=True)),
+ ('device', models.ForeignKey(to='servo.Device')),
+ ('order', models.ForeignKey(to='servo.Order')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='OrderStatus',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('started_at', models.DateTimeField()),
+ ('finished_at', models.DateTimeField(null=True)),
+ ('green_limit', models.DateTimeField(null=True)),
+ ('yellow_limit', models.DateTimeField(null=True)),
+ ('badge', models.CharField(default=b'undefined', max_length=16, choices=[(b'undefined', b'undefined'), (b'success', b'success'), (b'warning', b'warning'), (b'danger', b'danger')])),
+ ('finished_by', models.ForeignKey(related_name='+', to=settings.AUTH_USER_MODEL, null=True)),
+ ('order', models.ForeignKey(to='servo.Order')),
+ ('started_by', models.ForeignKey(related_name='+', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('-started_at',),
+ 'get_latest_by': 'started_at',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Payment',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('method', models.IntegerField(default=0, verbose_name='Payment Method', choices=[(0, 'No Charge'), (1, 'Cash'), (2, 'Invoice'), (3, 'Credit Card'), (4, 'Mail payment'), (5, 'Online payment')])),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('amount', models.DecimalField(max_digits=8, decimal_places=2)),
+ ('created_by', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+ ('invoice', models.ForeignKey(to='servo.Invoice')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Product',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('code', models.CharField(default=servo.lib.shorturl.from_time, unique=True, max_length=32, verbose_name='code')),
+ ('subst_code', models.CharField(default=b'', verbose_name='Substituted (new) code of this part', max_length=32, editable=False)),
+ ('title', models.CharField(default='New Product', max_length=255, verbose_name='Title')),
+ ('description', models.TextField(default=b'', verbose_name='Description', blank=True)),
+ ('pct_vat', models.DecimalField(default=servo.models.product.default_vat, verbose_name='VAT %', max_digits=4, decimal_places=2)),
+ ('fixed_price', models.BooleanField(default=False, help_text="Don't update price when recalculating prices or importing parts")),
+ ('price_purchase_exchange', models.DecimalField(default=0, verbose_name='Purchase price', max_digits=8, decimal_places=2)),
+ ('pct_margin_exchange', models.DecimalField(default=servo.models.product.get_margin, verbose_name='Margin %', max_digits=4, decimal_places=2)),
+ ('price_notax_exchange', models.DecimalField(default=0, help_text='Purchase price + margin %', verbose_name='Net price', max_digits=8, decimal_places=2)),
+ ('price_sales_exchange', models.DecimalField(default=0, help_text='Purchase price + margin % + shipping + VAT %', verbose_name='Sales price', max_digits=8, decimal_places=2)),
+ ('price_purchase_stock', models.DecimalField(default=0, verbose_name='Purchase price', max_digits=8, decimal_places=2)),
+ ('pct_margin_stock', models.DecimalField(default=servo.models.product.get_margin, verbose_name='Margin %', max_digits=4, decimal_places=2)),
+ ('price_notax_stock', models.DecimalField(default=0, help_text='Purchase price + margin %', verbose_name='Net price', max_digits=8, decimal_places=2)),
+ ('price_sales_stock', models.DecimalField(default=0, help_text='Purchase price + margin % + shipping + VAT %', verbose_name='Sales price', max_digits=8, decimal_places=2)),
+ ('is_serialized', models.BooleanField(default=False, help_text='Product has a serial number', verbose_name='is serialized')),
+ ('warranty_period', models.PositiveIntegerField(default=0, verbose_name='Warranty (months)')),
+ ('shelf', models.CharField(default=b'', max_length=8, verbose_name='Shelf code', blank=True)),
+ ('brand', models.CharField(default=b'', max_length=32, verbose_name='Brand', blank=True)),
+ ('photo', models.ImageField(upload_to=b'products', null=True, verbose_name='photo', blank=True)),
+ ('shipping', models.FloatField(default=0, verbose_name='shipping')),
+ ('component_code', models.CharField(default=b'', max_length=1, verbose_name='component group', blank=True, choices=[(b'0', b'General'), (b'1', b'Visual'), (b'2', b'Displays'), (b'3', b'Mass Storage'), (b'4', b'Input Devices'), (b'5', b'Boards'), (b'6', b'Power'), (b'7', b'Printer'), (b'8', b'Multi-function Device'), (b'9', b'Communication Devices'), (b'A', b'Share'), (b'B', b'iPhone'), (b'E', b'iPod'), (b'F', b'iPad')])),
+ ('labour_tier', models.CharField(default=b'', max_length=15, blank=True)),
+ ('part_type', models.CharField(default=b'OTHER', max_length=18, verbose_name='part type', choices=[(b'ADJUSTMENT', 'Adjustment'), (b'MODULE', 'Module'), (b'REPLACEMENT', 'Replacement'), (b'SERVICE', 'Service'), (b'SERVICE CONTRACT', 'Service Contract'), (b'OTHER', 'Other')])),
+ ('eee_code', models.CharField(default=b'', max_length=256, verbose_name='EEE code', blank=True)),
+ ('total_amount', models.IntegerField(default=0, editable=False)),
+ ],
+ options={
+ 'ordering': ('-id',),
+ 'permissions': (('change_amount', 'Can change product amount'),),
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ProductCategory',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Category', max_length=255)),
+ ('slug', models.SlugField(null=True, editable=False)),
+ ('lft', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('level', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('parent', mptt.fields.TreeForeignKey(related_name='children', blank=True, to='servo.ProductCategory', null=True)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ('-title',),
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Property',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Field', max_length=255, verbose_name='title')),
+ ('type', models.CharField(default=(b'customer', 'Customer'), max_length=32, verbose_name='type', choices=[(b'customer', 'Customer'), (b'order', 'Order'), (b'product', 'Product')])),
+ ('format', models.CharField(default=b'', max_length=32, verbose_name='format', blank=True)),
+ ('value', models.TextField(default=b'', verbose_name='value', blank=True)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ['title'],
+ 'verbose_name': 'Field',
+ 'verbose_name_plural': 'Fields',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='PurchaseOrder',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('reference', models.CharField(default=b'', max_length=32, verbose_name='reference', blank=True)),
+ ('confirmation', models.CharField(default=b'', max_length=32, verbose_name='confirmation', blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('submitted_at', models.DateTimeField(null=True, editable=False)),
+ ('supplier', models.CharField(max_length=32, verbose_name='supplier', blank=True)),
+ ('carrier', models.CharField(max_length=32, verbose_name='carrier', blank=True)),
+ ('tracking_id', models.CharField(max_length=128, verbose_name='tracking ID', blank=True)),
+ ('days_delivered', models.IntegerField(default=1, verbose_name='delivery Time', blank=True)),
+ ('has_arrived', models.BooleanField(default=False)),
+ ('total', models.FloatField(null=True, editable=False)),
+ ('invoice_id', models.CharField(default=b'', max_length=10, editable=False)),
+ ('invoice', models.FileField(help_text=b"Apple's sales invoice for this PO", upload_to=b'gsx_invoices', null=True, editable=False)),
+ ('created_by', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL)),
+ ('location', models.ForeignKey(editable=False, to='servo.Location', help_text='The location from which this PO was created')),
+ ('sales_order', models.ForeignKey(editable=False, to='servo.Order', null=True)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ('-id',),
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='PurchaseOrderItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('code', models.CharField(default=b'', max_length=128, blank=True)),
+ ('title', models.CharField(max_length=128, verbose_name='title')),
+ ('description', models.TextField(default=b'', verbose_name='description', blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('amount', models.IntegerField(default=1, verbose_name='amount')),
+ ('sn', models.CharField(default=b'', max_length=32, verbose_name='KGB Serial Number', blank=True)),
+ ('price', models.DecimalField(help_text='Purchase price without taxes', verbose_name='Purchase Price', max_digits=8, decimal_places=2)),
+ ('reference', models.CharField(default=b'', max_length=128, blank=True)),
+ ('ordered_at', models.DateTimeField(null=True, editable=False)),
+ ('expected_ship_date', models.DateField(null=True, editable=False)),
+ ('received_at', models.DateTimeField(verbose_name='arrived', null=True, editable=False, blank=True)),
+ ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ],
+ options={
+ 'ordering': ('id',),
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Queue',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Queue', max_length=255, verbose_name='Title')),
+ ('keywords', models.TextField(default=b'', help_text='Orders with devices matching these keywords will be automatically assigned to this queue', blank=True)),
+ ('description', models.TextField(verbose_name='description', blank=True)),
+ ('priority', models.IntegerField(default=1, verbose_name='priority', choices=[(2, 'High'), (1, 'Normal'), (0, 'Low')])),
+ ('gsx_soldto', models.CharField(default=b'', help_text='GSX queries of an order in this queue will be made using this Sold-To', max_length=10, verbose_name='Sold-To', blank=True)),
+ ('order_template', models.FileField(help_text='HTML template for Service Order/Work Confirmation', upload_to=b'templates', null=True, verbose_name='order template', blank=True)),
+ ('quote_template', models.FileField(help_text='HTML template for cost estimate', upload_to=b'templates', null=True, verbose_name='quote template', blank=True)),
+ ('receipt_template', models.FileField(help_text='HTML template for Sales Order Receipt', upload_to=b'templates', null=True, verbose_name='receipt template', blank=True)),
+ ('dispatch_template', models.FileField(help_text='HTML template for dispatched order', upload_to=b'templates', null=True, verbose_name='dispatch template', blank=True)),
+ ('locations', models.ManyToManyField(help_text='Pick the locations you want this queue to appear in.', to='servo.Location', verbose_name='locations')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ['title'],
+ 'verbose_name': 'Queue',
+ 'verbose_name_plural': 'Queues',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='QueueStatus',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('limit_green', models.IntegerField(default=1, verbose_name='green limit')),
+ ('limit_yellow', models.IntegerField(default=15, verbose_name='yellow limit')),
+ ('limit_factor', models.IntegerField(default=60, verbose_name='time unit', choices=[(60, 'Minutes'), (3600, 'Hours'), (86400, 'Days'), (604800, 'Weeks'), (2419200, 'Months')])),
+ ('queue', models.ForeignKey(to='servo.Queue')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='RatedItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('rating', models.PositiveIntegerField()),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Repair',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('tech_id', models.CharField(default=b'', max_length=15, verbose_name='Technician', blank=True)),
+ ('unit_received_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Unit Received')),
+ ('submitted_at', models.DateTimeField(null=True, editable=False)),
+ ('completed_at', models.DateTimeField(null=True, editable=False)),
+ ('request_review', models.BooleanField(default=False, help_text='Repair should be reviewed by Apple before confirmation')),
+ ('confirmation', models.CharField(default=b'', max_length=10, editable=False)),
+ ('reference', models.CharField(default=b'', max_length=16, verbose_name='Reference', blank=True)),
+ ('symptom', models.TextField()),
+ ('diagnosis', models.TextField()),
+ ('notes', models.TextField(default=b'', help_text='Notes are mandatory when requesting review.', blank=True, validators=[django.core.validators.MaxLengthValidator(800)])),
+ ('status', models.CharField(default=b'', max_length=128, editable=False)),
+ ('attachment', models.FileField(help_text='Choose files to be sent with the repair creation request', null=True, upload_to=b'repairs', blank=True)),
+ ('repair_number', models.CharField(default=b'', max_length=12, editable=False)),
+ ('mark_complete', models.BooleanField(default=False, help_text='Requires replacement serial number', verbose_name='mark complete')),
+ ('replacement_sn', models.CharField(default=b'', help_text='Serial Number of replacement part', max_length=18, verbose_name='New serial number', blank=True)),
+ ('repair_type', models.CharField(default=b'CA', max_length=2, editable=False, choices=[(b'CA', b'Carry-In/Non-Replinished'), (b'NE', b'Return Before Replace'), (b'NT', b'No Trouble Found'), (b'ON', b'Onsite (Indirect/Direct)'), (b'RR', b'Repair Or Replace/Whole Unit Mail-In'), (b'WH', b'Mail-In')])),
+ ('component_data', models.TextField(default=b'', editable=False)),
+ ('consumer_law', models.NullBooleanField(default=None, help_text='Unit is eligible for consumer law coverage')),
+ ('completed_by', models.ForeignKey(related_name='completed_repairs', on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ('created_by', models.ForeignKey(related_name='created_repairs', on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL)),
+ ('device', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to='servo.Device')),
+ ('gsx_account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to='servo.GsxAccount')),
+ ('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to='servo.Order')),
+ ],
+ options={
+ 'get_latest_by': 'created_at',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Search',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('query', models.TextField()),
+ ('model', models.CharField(max_length=32)),
+ ('title', models.CharField(max_length=128)),
+ ('shared', models.BooleanField(default=True)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ServiceOrderItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('code', models.CharField(default=b'', max_length=128, blank=True)),
+ ('title', models.CharField(max_length=128, verbose_name='title')),
+ ('description', models.TextField(default=b'', verbose_name='description', blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('amount', models.IntegerField(default=1, verbose_name='amount')),
+ ('sn', models.CharField(default=b'', max_length=32, verbose_name='KGB Serial Number', blank=True)),
+ ('dispatched', models.BooleanField(default=False, verbose_name='dispatched')),
+ ('should_report', models.BooleanField(default=True, verbose_name='report')),
+ ('price', models.DecimalField(verbose_name='sales price', max_digits=8, decimal_places=2)),
+ ('replaced_at', models.DateTimeField(null=True)),
+ ('kbb_sn', models.CharField(default=b'', max_length=32, verbose_name='KBB Serial Number', blank=True)),
+ ('imei', models.CharField(default=b'', max_length=35, verbose_name='IMEI', blank=True)),
+ ('price_category', models.CharField(default=(b'warranty', 'Warranty'), max_length=32, verbose_name='Price category', choices=[(b'warranty', 'Warranty'), (b'exchange', 'Exchange Price'), (b'stock', 'Stock Price')])),
+ ('comptia_code', models.CharField(default=b'', max_length=4, verbose_name='symptom code', blank=True)),
+ ('comptia_modifier', models.CharField(default=b'', max_length=1, verbose_name='symptom modifier', blank=True)),
+ ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ('order', models.ForeignKey(to='servo.Order')),
+ ('product', models.ForeignKey(to='servo.Product', on_delete=django.db.models.deletion.PROTECT)),
+ ('replaced_by', models.ForeignKey(related_name='replaced_parts', editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ],
+ options={
+ 'ordering': ('id',),
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ServicePart',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('comptia_code', models.CharField(verbose_name='Symptom Code', max_length=4, editable=False)),
+ ('comptia_modifier', models.CharField(verbose_name='Symptom Modifier', max_length=1, editable=False)),
+ ('line_number', models.SmallIntegerField(null=True, editable=False)),
+ ('registered_for_return', models.BooleanField(default=False)),
+ ('returned_at', models.DateTimeField(null=True, editable=False)),
+ ('ship_to', models.CharField(max_length=18, editable=False)),
+ ('part_title', models.CharField(max_length=128)),
+ ('part_number', models.CharField(max_length=18)),
+ ('service_order', models.CharField(max_length=10)),
+ ('return_order', models.CharField(default=b'', max_length=10)),
+ ('return_status', models.CharField(default=b'', max_length=128, editable=False)),
+ ('return_code', models.CharField(default=b'', max_length=4, editable=False)),
+ ('order_status', models.CharField(default=b'', max_length=128, editable=False)),
+ ('order_status_code', models.CharField(default=b'', max_length=4, editable=False)),
+ ('coverage_description', models.CharField(default=b'', max_length=128, editable=False)),
+ ('box_number', models.PositiveIntegerField(null=True)),
+ ('return_label', models.FileField(upload_to=b'return_labels', null=True, editable=False)),
+ ('carrier_url', models.CharField(default=b'', max_length=255, editable=False)),
+ ('order_item', models.ForeignKey(editable=False, to='servo.ServiceOrderItem')),
+ ('purchase_order', models.ForeignKey(editable=False, to='servo.PurchaseOrder', null=True)),
+ ('repair', models.ForeignKey(editable=False, to='servo.Repair')),
+ ],
+ options={
+ 'ordering': ('order_item',),
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Shipment',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('ship_to', models.CharField(default=b'', max_length=10, editable=False)),
+ ('return_id', models.CharField(help_text=b'The return ID returned by GSX', max_length=10, unique=True, null=True, editable=False)),
+ ('tracking_id', models.CharField(default=b'', help_text=b"Carrier's tracking ID", max_length=30, verbose_name='Tracking ID', blank=True)),
+ ('tracking_url', models.URLField(help_text=b'The tracking URL returned by GSX', null=True, editable=False)),
+ ('packing_list', models.FileField(help_text=b'The PDF returned by GSX', upload_to=b'returns', null=True, editable=False)),
+ ('carrier', models.CharField(default=b'', max_length=18, verbose_name='carrier', blank=True, choices=[(b'XAER', b'Aero 2000'), (b'XAIRBEC', b'Airborne'), (b'XAIRB', b'Airborne'), (b'XARM', b'Aramex'), (b'XOZP', b'Australia Post'), (b'XBAX', b'BAX GLOBAL PTE LTD'), (b'XCPW', b'CPW Internal'), (b'XCL', b'Citylink'), (b'XDHL', b'DHL'), (b'XDHLC', b'DHL'), (b'XDZNA', b'Danzas-AEI'), (b'XEAS', b'EAS'), (b'XEGL', b'Eagle ASIA PACIFIC HOLDINGS'), (b'XEXXN', b'Exel'), (b'XFEDE', b'FedEx'), (b'XFDE', b'FedEx Air'), (b'XGLS', b'GLS-General Logistics Systems'), (b'XHNF', b'H and Friends'), (b'XNGLN', b'Nightline'), (b'XPL', b'Parceline'), (b'XPRLA', b'Purolator'), (b'SDS', b'SDS An Post'), (b'XSNO', b'Seino Transportation Co. Ltd.'), (b'XSTE', b'Star Track Express'), (b'XTNT', b'TNT'), (b'XUPSN', b'UPS'), (b'XUTI', b'UTi (Japan) K.K.'), (b'XYMT', b'YAMATO')])),
+ ('created_at', models.DateTimeField(auto_now=True)),
+ ('dispatched_at', models.DateTimeField(null=True, editable=False)),
+ ('width', models.PositiveIntegerField(null=True, verbose_name='width', blank=True)),
+ ('height', models.PositiveIntegerField(null=True, verbose_name='height', blank=True)),
+ ('length', models.PositiveIntegerField(null=True, verbose_name='length', blank=True)),
+ ('weight', models.PositiveIntegerField(null=True, verbose_name='weight', blank=True)),
+ ('created_by', models.ForeignKey(related_name='created_shipments', on_delete=django.db.models.deletion.SET_NULL, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ('dispatched_by', models.ForeignKey(related_name='dispatched_shipments', editable=False, to=settings.AUTH_USER_MODEL, null=True)),
+ ('location', models.ForeignKey(editable=False, to='servo.Location')),
+ ],
+ options={
+ 'ordering': ('-dispatched_at',),
+ 'get_latest_by': 'id',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ShippingMethod',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Shipping Method', max_length=128)),
+ ('description', models.TextField(default=b'', blank=True)),
+ ('notify_email', models.EmailField(max_length=75, null=True, blank=True)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Status',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Status', max_length=255, verbose_name='name')),
+ ('description', models.TextField(null=True, verbose_name='description', blank=True)),
+ ('limit_green', models.IntegerField(default=1, verbose_name='green limit')),
+ ('limit_yellow', models.IntegerField(default=15, verbose_name='yellow limit')),
+ ('limit_factor', models.IntegerField(default=(60, 'Minutes'), verbose_name='time unit', choices=[(60, 'Minutes'), (3600, 'Hours'), (86400, 'Days'), (604800, 'Weeks'), (2419200, 'Months')])),
+ ('queue', models.ManyToManyField(to='servo.Queue', null=True, editable=False, through='servo.QueueStatus')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ('title',),
+ 'verbose_name': 'Status',
+ 'verbose_name_plural': 'Statuses',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Tag',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Tag', unique=True, max_length=255, verbose_name='name')),
+ ('type', models.CharField(max_length=32, verbose_name='type', choices=[(b'device', 'Device'), (b'order', 'Order'), (b'note', 'Note'), (b'other', 'Other')])),
+ ('times_used', models.IntegerField(default=0, editable=False)),
+ ('color', models.CharField(default=b'default', max_length=16, null=True, blank=True, choices=[(b'default', 'Default'), (b'success', 'Green'), (b'warning', 'Orange'), (b'important', 'Red'), (b'info', 'Blue')])),
+ ('lft', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('level', models.PositiveIntegerField(editable=False, db_index=True)),
+ ('parent', mptt.fields.TreeForeignKey(related_name='children', blank=True, to='servo.Tag', null=True)),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'verbose_name': 'Tag',
+ 'verbose_name_plural': 'Tags',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TaggedItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('tag', models.CharField(max_length=128)),
+ ('slug', models.SlugField()),
+ ('color', models.CharField(default=b'', max_length=8)),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Template',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(default='New Template', unique=True, max_length=128, verbose_name='title')),
+ ('content', models.TextField(verbose_name='content')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'ordering': ['title'],
+ 'verbose_name': 'Template',
+ 'verbose_name_plural': 'Templates',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TimedItem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('status', models.CharField(max_length=128)),
+ ('started_at', models.DateTimeField()),
+ ('timeout_at', models.DateTimeField()),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='UserGroup',
+ fields=[
+ ('group_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='auth.Group')),
+ ('site', models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site')),
+ ],
+ options={
+ },
+ bases=('auth.group',),
+ ),
+ migrations.AlterUniqueTogether(
+ name='taggeditem',
+ unique_together=set([('content_type', 'object_id', 'tag')]),
+ ),
+ migrations.AlterUniqueTogether(
+ name='status',
+ unique_together=set([('title', 'site')]),
+ ),
+ migrations.AddField(
+ model_name='servicepart',
+ name='shipment',
+ field=models.ForeignKey(to='servo.Shipment', null=True),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='servicepart',
+ unique_together=set([('id', 'shipment')]),
+ ),
+ migrations.AddField(
+ model_name='repair',
+ name='parts',
+ field=models.ManyToManyField(to='servo.ServiceOrderItem', through='servo.ServicePart'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='queuestatus',
+ name='status',
+ field=models.ForeignKey(to='servo.Status'),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='queuestatus',
+ unique_together=set([('queue', 'status')]),
+ ),
+ migrations.AddField(
+ model_name='queue',
+ name='status_assigned',
+ field=models.ForeignKey(related_name='+', blank=True, to='servo.QueueStatus', help_text='Order has ben assigned to a user', null=True, verbose_name='Order Assigned'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='queue',
+ name='status_closed',
+ field=models.ForeignKey(related_name='+', verbose_name='Order Closed', blank=True, to='servo.QueueStatus', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='queue',
+ name='status_created',
+ field=models.ForeignKey(related_name='+', blank=True, to='servo.QueueStatus', help_text='Order has ben placed to a queue', null=True, verbose_name='Order Created'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='queue',
+ name='status_dispatched',
+ field=models.ForeignKey(related_name='+', verbose_name='Order Dispatched', blank=True, to='servo.QueueStatus', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='queue',
+ name='status_products_ordered',
+ field=models.ForeignKey(related_name='+', blank=True, to='servo.QueueStatus', help_text='Purchase Order for this Service Order has been submitted', null=True, verbose_name='Products Ordered'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='queue',
+ name='status_products_received',
+ field=models.ForeignKey(related_name='+', blank=True, to='servo.QueueStatus', help_text='Products have been received', null=True, verbose_name='Products Received'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='queue',
+ name='status_repair_completed',
+ field=models.ForeignKey(related_name='+', blank=True, to='servo.QueueStatus', help_text='GSX repair completed', null=True, verbose_name='Repair Completed'),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='queue',
+ unique_together=set([('title', 'site')]),
+ ),
+ migrations.AddField(
+ model_name='purchaseorderitem',
+ name='order_item',
+ field=models.ForeignKey(editable=False, to='servo.ServiceOrderItem', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='purchaseorderitem',
+ name='product',
+ field=models.ForeignKey(to='servo.Product', on_delete=django.db.models.deletion.PROTECT),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='purchaseorderitem',
+ name='purchase_order',
+ field=models.ForeignKey(editable=False, to='servo.PurchaseOrder', verbose_name='Purchase Order'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='purchaseorderitem',
+ name='received_by',
+ field=models.ForeignKey(related_name='+', editable=False, to=settings.AUTH_USER_MODEL, null=True),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='productcategory',
+ unique_together=set([('title', 'site')]),
+ ),
+ migrations.AddField(
+ model_name='product',
+ name='categories',
+ field=models.ManyToManyField(to='servo.ProductCategory', null=True, verbose_name='Categories', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='product',
+ name='device_models',
+ field=models.ManyToManyField(to='servo.DeviceGroup', null=True, verbose_name='device models', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='product',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='orderstatus',
+ name='status',
+ field=models.ForeignKey(to='servo.Status'),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='orderdevice',
+ unique_together=set([('order', 'device')]),
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='devices',
+ field=models.ManyToManyField(to='servo.Device', null=True, through='servo.OrderDevice'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='followed_by',
+ field=models.ManyToManyField(related_name='followed_orders', to=settings.AUTH_USER_MODEL),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='location',
+ field=models.ForeignKey(to='servo.Location', on_delete=django.db.models.deletion.PROTECT),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='queue',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='queue', to='servo.Queue', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='started_by',
+ field=models.ForeignKey(related_name='started_orders', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='status',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='status', to='servo.QueueStatus', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='statuses',
+ field=models.ManyToManyField(related_name='orders', through='servo.OrderStatus', to='servo.Status'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='tags',
+ field=models.ManyToManyField(to='servo.Tag', verbose_name=b'tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='note',
+ name='labels',
+ field=models.ManyToManyField(to='servo.Tag', null=True, blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='note',
+ name='order',
+ field=models.ForeignKey(blank=True, to='servo.Order', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='note',
+ name='parent',
+ field=mptt.fields.TreeForeignKey(related_name='replies', blank=True, to='servo.Note', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='note',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='message',
+ name='note',
+ field=models.ForeignKey(to='servo.Note'),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='message',
+ unique_together=set([('note', 'recipient')]),
+ ),
+ migrations.AlterUniqueTogether(
+ name='location',
+ unique_together=set([('title', 'site')]),
+ ),
+ migrations.AddField(
+ model_name='invoiceitem',
+ name='product',
+ field=models.ForeignKey(to='servo.Product', on_delete=django.db.models.deletion.PROTECT),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='invoice',
+ name='order',
+ field=models.ForeignKey(editable=False, to='servo.Order'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='invoice',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='inventory',
+ name='location',
+ field=models.ForeignKey(to='servo.Location'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='inventory',
+ name='product',
+ field=models.ForeignKey(to='servo.Product'),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='inventory',
+ unique_together=set([('product', 'location')]),
+ ),
+ migrations.AlterUniqueTogether(
+ name='gsxaccount',
+ unique_together=set([('sold_to', 'ship_to', 'environment', 'site')]),
+ ),
+ migrations.AddField(
+ model_name='escalation',
+ name='gsx_account',
+ field=models.ForeignKey(default=servo.defaults.gsx_account, verbose_name='GSX Account', to='servo.GsxAccount'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='device',
+ name='products',
+ field=models.ManyToManyField(help_text='Products that are compatible with this device instance', to='servo.Product', null=True, editable=False),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='device',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='customer',
+ name='devices',
+ field=models.ManyToManyField(verbose_name='devices', null=True, editable=False, to='servo.Device', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='customer',
+ name='groups',
+ field=models.ManyToManyField(to='servo.CustomerGroup', null=True, verbose_name='Groups', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='customer',
+ name='parent',
+ field=mptt.fields.TreeForeignKey(related_name='contacts', verbose_name='company', blank=True, to='servo.Customer', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='customer',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='customer',
+ name='tags',
+ field=models.ManyToManyField(to='servo.Tag', null=True, verbose_name='tags', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='contactinfo',
+ name='customer',
+ field=models.ForeignKey(to='servo.Customer'),
+ preserve_default=True,
+ ),
+ migrations.AlterUniqueTogether(
+ name='contactinfo',
+ unique_together=set([('customer', 'key')]),
+ ),
+ migrations.AlterUniqueTogether(
+ name='configuration',
+ unique_together=set([('key', 'site')]),
+ ),
+ migrations.AddField(
+ model_name='checklistitemvalue',
+ name='order',
+ field=models.ForeignKey(to='servo.Order'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='checklist',
+ name='queues',
+ field=models.ManyToManyField(to='servo.Queue', null=True, verbose_name='queue', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='checklist',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='accessory',
+ name='device',
+ field=models.ForeignKey(to='servo.Device'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='accessory',
+ name='order',
+ field=models.ForeignKey(to='servo.Order'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='customer',
+ field=mptt.fields.TreeForeignKey(blank=True, editable=False, to='servo.Customer', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='groups',
+ field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='location',
+ field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.PROTECT, verbose_name='Current Location', to='servo.Location', help_text='Orders you create will be registered to this location.', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='locations',
+ field=models.ManyToManyField(to='servo.Location', null=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='queues',
+ field=models.ManyToManyField(to='servo.Queue', null=True, verbose_name='queues', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='site',
+ field=models.ForeignKey(default=servo.defaults.site_id, editable=False, to='sites.Site'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='user_permissions',
+ field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0002_auto_20141123_2358.py b/servo/migrations/0002_auto_20141123_2358.py
new file mode 100644
index 0000000..3c24ad4
--- /dev/null
+++ b/servo/migrations/0002_auto_20141123_2358.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import servo.defaults
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='message',
+ name='code',
+ field=models.CharField(default=servo.defaults.uid, unique=True, max_length=36),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0003_auto_20141217_0029.py b/servo/migrations/0003_auto_20141217_0029.py
new file mode 100644
index 0000000..cdab1af
--- /dev/null
+++ b/servo/migrations/0003_auto_20141217_0029.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.db.models.deletion
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0002_auto_20141123_2358'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='customer',
+ name='is_company',
+ field=models.BooleanField(default=False, help_text='Companies can contain contacts', verbose_name='company'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='warranty_status',
+ field=models.CharField(default=b'NA', max_length=3, verbose_name='Warranty Status', choices=[(b'QP', 'Quality Program'), (b'CS', 'Customer Satisfaction'), (b'ALW', 'Apple Limited Warranty'), (b'APP', 'AppleCare Protection Plan'), (b'CC', 'Custom Bid Contracts'), (b'CBC', 'Custom Bid Contracts'), (b'WTY', "3'rd Party Warranty"), (b'OOW', 'Out Of Warranty (No Coverage)'), (b'NA', 'Unknown')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='closed_by',
+ field=models.ForeignKey(related_name='closed_orders', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='product',
+ name='component_code',
+ field=models.CharField(default=b'', max_length=1, verbose_name='component group', blank=True, choices=[(b'0', b'General'), (b'1', b'Visual'), (b'2', b'Displays'), (b'3', b'Mass Storage'), (b'4', b'Input Devices'), (b'5', b'Boards'), (b'6', b'Power'), (b'7', b'Printer'), (b'8', b'Multi-function Device'), (b'9', b'Communication Devices'), (b'A', b'Share'), (b'B', b'iPhone'), (b'E', b'iPod'), (b'F', b'iPad'), (b'G', b'Beats Products')]),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0004_auto_20141229_0930.py b/servo/migrations/0004_auto_20141229_0930.py
new file mode 100644
index 0000000..ab02c19
--- /dev/null
+++ b/servo/migrations/0004_auto_20141229_0930.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0003_auto_20141217_0029'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='accessory',
+ name='name',
+ field=models.TextField(),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0004_auto_20141230_1630.py b/servo/migrations/0004_auto_20141230_1630.py
new file mode 100644
index 0000000..825d7cb
--- /dev/null
+++ b/servo/migrations/0004_auto_20141230_1630.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import servo.validators
+import servo.defaults
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0003_auto_20141217_0029'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='checklistitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='Enhet', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='country',
+ field=models.CharField(default=servo.defaults.country, max_length=2, verbose_name='Land', blank=True, choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'Samoa (American)'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AX', 'Aaland Islands'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia & Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BL', 'St Barthelemy'), ('BM', 'Bermuda'), ('BN', 'Brunei'), ('BO', 'Bolivia'), ('BQ', 'Caribbean Netherlands'), ('BR', 'Brazil'), ('BS', 'Bahamas'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CD', 'Congo (Dem. Rep.)'), ('CF', 'Central African Rep.'), ('CG', 'Congo (Rep.)'), ('CH', 'Switzerland'), ('CI', "Cote d'Ivoire"), ('CK', 'Cook Islands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CW', 'Curacao'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('GA', 'Gabon'), ('GB', 'Britain (UK)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GG', 'Guernsey'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia & the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard Island & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IM', 'Isle of Man'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JE', 'Jersey'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St Kitts & Nevis'), ('KP', 'Korea (North)'), ('KR', 'Korea (South)'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', 'Laos'), ('LB', 'Lebanon'), ('LC', 'St Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova'), ('ME', 'Montenegro'), ('MF', 'St Martin (French part)'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('MK', 'Macedonia'), ('ML', 'Mali'), ('MM', 'Myanmar (Burma)'), ('MN', 'Mongolia'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Montserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PS', 'Palestine'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RS', 'Serbia'), ('RU', 'Russia'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('SS', 'South Sudan'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SX', 'St Maarten (Dutch part)'), ('SY', 'Syria'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Is'), ('TD', 'Chad'), ('TF', 'French Southern & Antarctic Lands'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TL', 'East Timor'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan'), ('TZ', 'Tanzania'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'US minor outlying islands'), ('US', 'United States'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City'), ('VC', 'St Vincent'), ('VE', 'Venezuela'), ('VG', 'Virgin Islands (UK)'), ('VI', 'Virgin Islands (US)'), ('VN', 'Vietnam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna'), ('WS', 'Samoa (western)'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='name',
+ field=models.CharField(default='New Customer', max_length=255, verbose_name='namn'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='phone',
+ field=models.CharField(default=b'', max_length=32, verbose_name='telefon', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='zip_code',
+ field=models.CharField(default=b'', max_length=32, verbose_name='Postnummer', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customergroup',
+ name='name',
+ field=models.CharField(default='New Group', unique=True, max_length=255, verbose_name='namn'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='description',
+ field=models.CharField(default='New Device', max_length=128, verbose_name='Enhet'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='password',
+ field=models.CharField(default=b'', max_length=32, verbose_name='l\xf6senord', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='purchased_on',
+ field=models.DateField(null=True, verbose_name='Ink\xf6psdatum', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='sn',
+ field=models.CharField(default=b'', max_length=32, verbose_name='Serienummer', blank=True, validators=[servo.validators.sn_validator]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='username',
+ field=models.CharField(default=b'', max_length=32, verbose_name='anv\xe4ndarnamn', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='warranty_status',
+ field=models.CharField(default=b'NA', max_length=3, verbose_name='Garantistatus', choices=[(b'QP', 'Quality Program'), (b'CS', 'Customer Satisfaction'), (b'ALW', 'Apple Limited Warranty'), (b'APP', 'AppleCare Protection Plan'), (b'CC', 'Custom Bid Contracts'), (b'CBC', 'Custom Bid Contracts'), (b'WTY', "3'rd Party Warranty"), (b'OOW', 'Out Of Warranty (No Coverage)'), (b'NA', 'Ok\xe4nd')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='gsxaccount',
+ name='password',
+ field=models.CharField(default=b'', max_length=256, verbose_name='L\xf6senord', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_address',
+ field=models.CharField(max_length=255, null=True, verbose_name='Gatuadress', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_email',
+ field=models.CharField(max_length=128, null=True, verbose_name='E-postadress', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_name',
+ field=models.CharField(default='Walk-in', max_length=255, verbose_name='Namn'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_phone',
+ field=models.CharField(max_length=128, null=True, verbose_name='Telefon', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='payment_method',
+ field=models.IntegerField(default=0, verbose_name='Payment Method', editable=False, choices=[(0, 'Utan kostnad'), (1, 'Cash'), (2, 'Faktura'), (3, 'Kreditkort'), (4, 'Mail payment'), (5, 'Online payment')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoiceitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='Enhet', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='notes',
+ field=models.TextField(default=b'9:00 - 18:00', help_text='Will be shown on print templates', verbose_name='Anm\xe4rkningar', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='phone',
+ field=models.CharField(default=b'', max_length=32, verbose_name='telefon', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='title',
+ field=models.CharField(default='New Location', max_length=255, verbose_name='namn'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='zip_code',
+ field=models.CharField(default=b'', max_length=8, verbose_name='Postnummer', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='payment',
+ name='method',
+ field=models.IntegerField(default=0, verbose_name='Payment Method', choices=[(0, 'Utan kostnad'), (1, 'Cash'), (2, 'Faktura'), (3, 'Kreditkort'), (4, 'Mail payment'), (5, 'Online payment')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='product',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='Enhet', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='property',
+ name='type',
+ field=models.CharField(default=(b'customer', 'Kund'), max_length=32, verbose_name='type', choices=[(b'customer', 'Kund'), (b'order', 'Arbet'), (b'product', 'Product')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='purchaseorderitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='Enhet', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='queue',
+ name='description',
+ field=models.TextField(verbose_name='Enhet', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='repair',
+ name='replacement_sn',
+ field=models.CharField(default=b'', help_text='Serial Number of replacement part', max_length=18, verbose_name='Nya serienummer', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='serviceorderitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='Enhet', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='serviceorderitem',
+ name='price_category',
+ field=models.CharField(default=(b'warranty', 'Garanti'), max_length=32, verbose_name='Priskategorin', choices=[(b'warranty', 'Garanti'), (b'exchange', 'Exchange Price'), (b'stock', 'Stock Price')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='status',
+ name='description',
+ field=models.TextField(null=True, verbose_name='Enhet', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='status',
+ name='title',
+ field=models.CharField(default='New Status', max_length=255, verbose_name='namn'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='tag',
+ name='title',
+ field=models.CharField(default='New Tag', unique=True, max_length=255, verbose_name='namn'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='tag',
+ name='type',
+ field=models.CharField(max_length=32, verbose_name='type', choices=[(b'device', 'Device'), (b'order', 'Arbet'), (b'note', 'Note'), (b'other', 'Other')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='gsx_password',
+ field=models.CharField(default=b'', max_length=256, verbose_name='L\xf6senord', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='locale',
+ field=models.CharField(default=b'da_DK.UTF-8', help_text='Select which language you want to use Servo in.', max_length=32, verbose_name='language', choices=[(b'da_DK.UTF-8', 'Danska'), (b'nl_NL.UTF-8', 'Holl\xe4ndska'), (b'en_US.UTF-8', 'Engelska'), (b'et_EE.UTF-8', 'Estl\xe4ndska'), (b'fi_FI.UTF-8', 'Finska'), (b'sv_SE.UTF-8', 'Svenska')]),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0005_auto_20141229_2045.py b/servo/migrations/0005_auto_20141229_2045.py
new file mode 100644
index 0000000..80c65e0
--- /dev/null
+++ b/servo/migrations/0005_auto_20141229_2045.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0004_auto_20141229_0930'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='order',
+ name='customer',
+ field=models.ForeignKey(related_name='orders', on_delete=django.db.models.deletion.SET_NULL, to='servo.Customer', null=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0006_merge.py b/servo/migrations/0006_merge.py
new file mode 100644
index 0000000..99f1291
--- /dev/null
+++ b/servo/migrations/0006_merge.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0004_auto_20141230_1630'),
+ ('servo', '0005_auto_20141229_2045'),
+ ]
+
+ operations = [
+ ]
diff --git a/servo/migrations/0007_auto_20150101_2318.py b/servo/migrations/0007_auto_20150101_2318.py
new file mode 100644
index 0000000..d787944
--- /dev/null
+++ b/servo/migrations/0007_auto_20150101_2318.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import servo.validators
+import servo.defaults
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0006_merge'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='checklistitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='Description', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='country',
+ field=models.CharField(default=servo.defaults.country, max_length=2, verbose_name='Country', blank=True, choices=[('AD', 'Andorra'), ('AE', 'United Arab Emirates'), ('AF', 'Afghanistan'), ('AG', 'Antigua & Barbuda'), ('AI', 'Anguilla'), ('AL', 'Albania'), ('AM', 'Armenia'), ('AO', 'Angola'), ('AQ', 'Antarctica'), ('AR', 'Argentina'), ('AS', 'Samoa (American)'), ('AT', 'Austria'), ('AU', 'Australia'), ('AW', 'Aruba'), ('AX', 'Aaland Islands'), ('AZ', 'Azerbaijan'), ('BA', 'Bosnia & Herzegovina'), ('BB', 'Barbados'), ('BD', 'Bangladesh'), ('BE', 'Belgium'), ('BF', 'Burkina Faso'), ('BG', 'Bulgaria'), ('BH', 'Bahrain'), ('BI', 'Burundi'), ('BJ', 'Benin'), ('BL', 'St Barthelemy'), ('BM', 'Bermuda'), ('BN', 'Brunei'), ('BO', 'Bolivia'), ('BQ', 'Caribbean Netherlands'), ('BR', 'Brazil'), ('BS', 'Bahamas'), ('BT', 'Bhutan'), ('BV', 'Bouvet Island'), ('BW', 'Botswana'), ('BY', 'Belarus'), ('BZ', 'Belize'), ('CA', 'Canada'), ('CC', 'Cocos (Keeling) Islands'), ('CD', 'Congo (Dem. Rep.)'), ('CF', 'Central African Rep.'), ('CG', 'Congo (Rep.)'), ('CH', 'Switzerland'), ('CI', "Cote d'Ivoire"), ('CK', 'Cook Islands'), ('CL', 'Chile'), ('CM', 'Cameroon'), ('CN', 'China'), ('CO', 'Colombia'), ('CR', 'Costa Rica'), ('CU', 'Cuba'), ('CV', 'Cape Verde'), ('CW', 'Curacao'), ('CX', 'Christmas Island'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DE', 'Germany'), ('DJ', 'Djibouti'), ('DK', 'Denmark'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('DZ', 'Algeria'), ('EC', 'Ecuador'), ('EE', 'Estonia'), ('EG', 'Egypt'), ('EH', 'Western Sahara'), ('ER', 'Eritrea'), ('ES', 'Spain'), ('ET', 'Ethiopia'), ('FI', 'Finland'), ('FJ', 'Fiji'), ('FK', 'Falkland Islands'), ('FM', 'Micronesia'), ('FO', 'Faroe Islands'), ('FR', 'France'), ('GA', 'Gabon'), ('GB', 'Britain (UK)'), ('GD', 'Grenada'), ('GE', 'Georgia'), ('GF', 'French Guiana'), ('GG', 'Guernsey'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GL', 'Greenland'), ('GM', 'Gambia'), ('GN', 'Guinea'), ('GP', 'Guadeloupe'), ('GQ', 'Equatorial Guinea'), ('GR', 'Greece'), ('GS', 'South Georgia & the South Sandwich Islands'), ('GT', 'Guatemala'), ('GU', 'Guam'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HK', 'Hong Kong'), ('HM', 'Heard Island & McDonald Islands'), ('HN', 'Honduras'), ('HR', 'Croatia'), ('HT', 'Haiti'), ('HU', 'Hungary'), ('ID', 'Indonesia'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IM', 'Isle of Man'), ('IN', 'India'), ('IO', 'British Indian Ocean Territory'), ('IQ', 'Iraq'), ('IR', 'Iran'), ('IS', 'Iceland'), ('IT', 'Italy'), ('JE', 'Jersey'), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('JP', 'Japan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('KH', 'Cambodia'), ('KI', 'Kiribati'), ('KM', 'Comoros'), ('KN', 'St Kitts & Nevis'), ('KP', 'Korea (North)'), ('KR', 'Korea (South)'), ('KW', 'Kuwait'), ('KY', 'Cayman Islands'), ('KZ', 'Kazakhstan'), ('LA', 'Laos'), ('LB', 'Lebanon'), ('LC', 'St Lucia'), ('LI', 'Liechtenstein'), ('LK', 'Sri Lanka'), ('LR', 'Liberia'), ('LS', 'Lesotho'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('LV', 'Latvia'), ('LY', 'Libya'), ('MA', 'Morocco'), ('MC', 'Monaco'), ('MD', 'Moldova'), ('ME', 'Montenegro'), ('MF', 'St Martin (French part)'), ('MG', 'Madagascar'), ('MH', 'Marshall Islands'), ('MK', 'Macedonia'), ('ML', 'Mali'), ('MM', 'Myanmar (Burma)'), ('MN', 'Mongolia'), ('MO', 'Macau'), ('MP', 'Northern Mariana Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MS', 'Montserrat'), ('MT', 'Malta'), ('MU', 'Mauritius'), ('MV', 'Maldives'), ('MW', 'Malawi'), ('MX', 'Mexico'), ('MY', 'Malaysia'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('NC', 'New Caledonia'), ('NE', 'Niger'), ('NF', 'Norfolk Island'), ('NG', 'Nigeria'), ('NI', 'Nicaragua'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NP', 'Nepal'), ('NR', 'Nauru'), ('NU', 'Niue'), ('NZ', 'New Zealand'), ('OM', 'Oman'), ('PA', 'Panama'), ('PE', 'Peru'), ('PF', 'French Polynesia'), ('PG', 'Papua New Guinea'), ('PH', 'Philippines'), ('PK', 'Pakistan'), ('PL', 'Poland'), ('PM', 'St Pierre & Miquelon'), ('PN', 'Pitcairn'), ('PR', 'Puerto Rico'), ('PS', 'Palestine'), ('PT', 'Portugal'), ('PW', 'Palau'), ('PY', 'Paraguay'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RS', 'Serbia'), ('RU', 'Russia'), ('RW', 'Rwanda'), ('SA', 'Saudi Arabia'), ('SB', 'Solomon Islands'), ('SC', 'Seychelles'), ('SD', 'Sudan'), ('SE', 'Sweden'), ('SG', 'Singapore'), ('SH', 'St Helena'), ('SI', 'Slovenia'), ('SJ', 'Svalbard & Jan Mayen'), ('SK', 'Slovakia'), ('SL', 'Sierra Leone'), ('SM', 'San Marino'), ('SN', 'Senegal'), ('SO', 'Somalia'), ('SR', 'Suriname'), ('SS', 'South Sudan'), ('ST', 'Sao Tome & Principe'), ('SV', 'El Salvador'), ('SX', 'St Maarten (Dutch part)'), ('SY', 'Syria'), ('SZ', 'Swaziland'), ('TC', 'Turks & Caicos Is'), ('TD', 'Chad'), ('TF', 'French Southern & Antarctic Lands'), ('TG', 'Togo'), ('TH', 'Thailand'), ('TJ', 'Tajikistan'), ('TK', 'Tokelau'), ('TL', 'East Timor'), ('TM', 'Turkmenistan'), ('TN', 'Tunisia'), ('TO', 'Tonga'), ('TR', 'Turkey'), ('TT', 'Trinidad & Tobago'), ('TV', 'Tuvalu'), ('TW', 'Taiwan'), ('TZ', 'Tanzania'), ('UA', 'Ukraine'), ('UG', 'Uganda'), ('UM', 'US minor outlying islands'), ('US', 'United States'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VA', 'Vatican City'), ('VC', 'St Vincent'), ('VE', 'Venezuela'), ('VG', 'Virgin Islands (UK)'), ('VI', 'Virgin Islands (US)'), ('VN', 'Vietnam'), ('VU', 'Vanuatu'), ('WF', 'Wallis & Futuna'), ('WS', 'Samoa (western)'), ('YE', 'Yemen'), ('YT', 'Mayotte'), ('ZA', 'South Africa'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='name',
+ field=models.CharField(default='New Customer', max_length=255, verbose_name='name'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='phone',
+ field=models.CharField(default=b'', max_length=32, verbose_name='phone', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='zip_code',
+ field=models.CharField(default=b'', max_length=32, verbose_name='ZIP Code', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='customergroup',
+ name='name',
+ field=models.CharField(default='New Group', unique=True, max_length=255, verbose_name='name'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='description',
+ field=models.CharField(default='New Device', max_length=128, verbose_name='description'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='password',
+ field=models.CharField(default=b'', max_length=32, verbose_name='password', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='purchased_on',
+ field=models.DateField(null=True, verbose_name='Date Purchased', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='sn',
+ field=models.CharField(default=b'', max_length=32, verbose_name='Serial Number', blank=True, validators=[servo.validators.sn_validator]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='username',
+ field=models.CharField(default=b'', max_length=32, verbose_name='username', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='warranty_status',
+ field=models.CharField(default=b'NA', max_length=3, verbose_name='Warranty Status', choices=[(b'QP', 'Quality Program'), (b'CS', 'Customer Satisfaction'), (b'ALW', 'Apple Limited Warranty'), (b'APP', 'AppleCare Protection Plan'), (b'CC', 'Custom Bid Contracts'), (b'CBC', 'Custom Bid Contracts'), (b'WTY', "3'rd Party Warranty"), (b'OOW', 'Out Of Warranty (No Coverage)'), (b'NA', 'Unknown')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='gsxaccount',
+ name='password',
+ field=models.CharField(default=b'', max_length=256, verbose_name='Password', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_address',
+ field=models.CharField(max_length=255, null=True, verbose_name='Address', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_email',
+ field=models.CharField(max_length=128, null=True, verbose_name='Email', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_name',
+ field=models.CharField(default='Walk-in', max_length=255, verbose_name='Name'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='customer_phone',
+ field=models.CharField(max_length=128, null=True, verbose_name='Phone', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='payment_method',
+ field=models.IntegerField(default=0, verbose_name='Payment Method', editable=False, choices=[(0, 'No Charge'), (1, 'Cash'), (2, 'Invoice'), (3, 'Credit Card'), (4, 'Mail payment'), (5, 'Online payment')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='invoiceitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='description', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='notes',
+ field=models.TextField(default=b'9:00 - 18:00', help_text='Will be shown on print templates', verbose_name='Notes', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='phone',
+ field=models.CharField(default=b'', max_length=32, verbose_name='phone', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='title',
+ field=models.CharField(default='New Location', max_length=255, verbose_name='name'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='zip_code',
+ field=models.CharField(default=b'', max_length=8, verbose_name='ZIP Code', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='payment',
+ name='method',
+ field=models.IntegerField(default=0, verbose_name='Payment Method', choices=[(0, 'No Charge'), (1, 'Cash'), (2, 'Invoice'), (3, 'Credit Card'), (4, 'Mail payment'), (5, 'Online payment')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='product',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='Description', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='property',
+ name='type',
+ field=models.CharField(default=(b'customer', 'Customer'), max_length=32, verbose_name='type', choices=[(b'customer', 'Customer'), (b'order', 'Order'), (b'product', 'Product')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='purchaseorderitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='description', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='queue',
+ name='description',
+ field=models.TextField(verbose_name='description', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='repair',
+ name='replacement_sn',
+ field=models.CharField(default=b'', help_text='Serial Number of replacement part', max_length=18, verbose_name='New serial number', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='serviceorderitem',
+ name='description',
+ field=models.TextField(default=b'', verbose_name='description', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='serviceorderitem',
+ name='price_category',
+ field=models.CharField(default=(b'warranty', 'Warranty'), max_length=32, verbose_name='Price category', choices=[(b'warranty', 'Warranty'), (b'exchange', 'Exchange Price'), (b'stock', 'Stock Price')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='status',
+ name='description',
+ field=models.TextField(null=True, verbose_name='description', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='status',
+ name='title',
+ field=models.CharField(default='New Status', max_length=255, verbose_name='name'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='tag',
+ name='title',
+ field=models.CharField(default='New Tag', unique=True, max_length=255, verbose_name='name'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='tag',
+ name='type',
+ field=models.CharField(max_length=32, verbose_name='type', choices=[(b'device', 'Device'), (b'order', 'Order'), (b'note', 'Note'), (b'other', 'Other')]),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='gsx_password',
+ field=models.CharField(default=b'', max_length=256, verbose_name='Password', blank=True),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='locale',
+ field=models.CharField(default=b'da_DK.UTF-8', help_text='Select which language you want to use Servo in.', max_length=32, verbose_name='language', choices=[(b'da_DK.UTF-8', 'Danish'), (b'nl_NL.UTF-8', 'Dutch'), (b'en_US.UTF-8', 'English'), (b'et_EE.UTF-8', 'Estonian'), (b'fi_FI.UTF-8', 'Finnish'), (b'sv_SE.UTF-8', 'Swedish')]),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0008_user_enable_notifications.py b/servo/migrations/0008_user_enable_notifications.py
new file mode 100644
index 0000000..ce121dc
--- /dev/null
+++ b/servo/migrations/0008_user_enable_notifications.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0007_auto_20150101_2318'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='enable_notifications',
+ field=models.BooleanField(default=True, help_text='Enable notifications in the toolbar.', verbose_name='Enable notifications'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0009_auto_20150101_2336.py b/servo/migrations/0009_auto_20150101_2336.py
new file mode 100644
index 0000000..f0d8df2
--- /dev/null
+++ b/servo/migrations/0009_auto_20150101_2336.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0008_user_enable_notifications'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='user',
+ old_name='enable_notifications',
+ new_name='should_notify',
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='timezone',
+ field=models.CharField(default=b'UTC', help_text='Your current timezone', max_length=128, verbose_name='Time zone', choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montreal', b'America/Montreal'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Santa_Isabel', b'America/Santa_Isabel'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Rangoon', b'Asia/Rangoon'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GMT', b'GMT'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Johnston', b'Pacific/Johnston'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'US/Alaska', b'US/Alaska'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'UTC', b'UTC')]),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0010_auto_20150104_0018.py b/servo/migrations/0010_auto_20150104_0018.py
new file mode 100644
index 0000000..eec1c1f
--- /dev/null
+++ b/servo/migrations/0010_auto_20150104_0018.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import servo.defaults
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0009_auto_20150101_2336'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='note',
+ name='subject',
+ field=models.CharField(default=servo.defaults.subject, max_length=255, verbose_name='Subject', blank=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0011_device_next_tether_policy.py b/servo/migrations/0011_device_next_tether_policy.py
new file mode 100644
index 0000000..3222610
--- /dev/null
+++ b/servo/migrations/0011_device_next_tether_policy.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0010_auto_20150104_0018'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='device',
+ name='next_tether_policy',
+ field=models.CharField(default=b'', verbose_name='Next Tether Policy', max_length=128, editable=False),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0012_auto_20150107_1711.py b/servo/migrations/0012_auto_20150107_1711.py
new file mode 100644
index 0000000..e3ef0c7
--- /dev/null
+++ b/servo/migrations/0012_auto_20150107_1711.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0011_device_next_tether_policy'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='device',
+ name='product_line',
+ field=models.CharField(default=b'OTHER', max_length=16, verbose_name='Product Line', choices=[(b'IPODCLASSIC', b'iPod Classic'), (b'POWERMAC', b'Power Mac'), (b'APPLETV', b'Apple TV'), (b'IMAC', b'iMac'), (b'OTHER', b'Other Products'), (b'MACBOOKAIR', b'MacBook Air'), (b'DISPLAYS', b'Display'), (b'IPODTOUCH', b'iPod Touch'), (b'MACPRO', b'Mac Pro'), (b'IPODNANO', b'iPod nano'), (b'IPAD', b'iPad'), (b'MACBOOK', b'MacBook'), (b'MACACCESSORY', b'Mac Accessory'), (b'MACMINI', b'Mac mini'), (b'SERVER', b'Server'), (b'BEATS', b'Beats Products'), (b'IPHONE', b'iPhone'), (b'IPHONEACCESSORY', b'iPhone Accessory'), (b'IPODSHUFFLE', b'iPod Shuffle'), (b'MACBOOKPRO', b'MacBook Pro')]),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0013_auto_20150204_1113.py b/servo/migrations/0013_auto_20150204_1113.py
new file mode 100644
index 0000000..baea37c
--- /dev/null
+++ b/servo/migrations/0013_auto_20150204_1113.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import servo.defaults
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0012_auto_20150107_1711'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='order',
+ options={'ordering': ('-priority', 'id'), 'permissions': (('change_user', 'Can set assignee'), ('change_status', 'Can change status'), ('follow_order', 'Can follow order'), ('copy_order', 'Can copy order'))},
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='purchase_country',
+ field=models.CharField(default=servo.defaults.country, editable=False, choices=[('AF', 'Afghanistan'), ('AX', '\xc5land Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('CV', 'Cabo Verde'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo (the Democratic Republic of the)'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "C\xf4te d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CW', 'Cura\xe7ao'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands [Malvinas]'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia (The)'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See [Vatican City State]'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', 'Laos'), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia (the Federated States of)'), ('MD', 'Moldovia'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('KP', 'North Korea'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestine, State of'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'R\xe9union'), ('RO', 'Romania'), ('RU', 'Russia'), ('RW', 'Rwanda'), ('BL', 'Saint Barth\xe9lemy'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin (French part)'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SX', 'Sint Maarten (Dutch part)'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('KR', 'South Korea'), ('SS', 'South Sudan'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syria'), ('TW', 'Taiwan'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('GB', 'United Kingdom'), ('US', 'United States'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Vietnam'), ('VG', 'Virgin Islands (British)'), ('VI', 'Virgin Islands (U.S.)'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], max_length=128, blank=True, verbose_name='Purchase Country'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0014_orderstatus_duration.py b/servo/migrations/0014_orderstatus_duration.py
new file mode 100644
index 0000000..f25f740
--- /dev/null
+++ b/servo/migrations/0014_orderstatus_duration.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0013_auto_20150204_1113'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='orderstatus',
+ name='duration',
+ field=models.IntegerField(default=0),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0015_auto_20150208_1629.py b/servo/migrations/0015_auto_20150208_1629.py
new file mode 100644
index 0000000..e42ba7d
--- /dev/null
+++ b/servo/migrations/0015_auto_20150208_1629.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0014_orderstatus_duration'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='orderstatus',
+ name='started_at',
+ field=models.DateTimeField(auto_now_add=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0016_auto_20150316_1152.py b/servo/migrations/0016_auto_20150316_1152.py
new file mode 100644
index 0000000..018d758
--- /dev/null
+++ b/servo/migrations/0016_auto_20150316_1152.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import mptt.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0015_auto_20150208_1629'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Action',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('key', models.CharField(default=b'SEND_EMAIL', max_length=32, choices=[(b'SEND_SMS', 'Send SMS'), (b'SEND_EMAIL', 'Send email'), (b'ADD_TAG', 'Add Tag'), (b'SET_PRIO', 'Set Priority'), (b'SET_QUEUE', 'Set Queue'), (b'SET_USER', 'Assign to')])),
+ ('value', models.TextField(default=b'')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Condition',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('key', models.CharField(max_length=16, choices=[(b'QUEUE', 'Queue'), (b'STATUS', 'Status'), (b'CUSTOMER_NAME', 'Customer name'), (b'DEVICE', 'Device name')])),
+ ('operator', models.CharField(max_length=4, choices=[(b'^%s$', 'Equals'), (b'%s', 'Contains'), (b'%d < %d', 'Less than'), (b'%d > %d', 'Greater than')])),
+ ('value', models.TextField(default=b'')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Rule',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('description', models.CharField(default='New Rule', max_length=128)),
+ ('match', models.CharField(default=b'ANY', max_length=3, choices=[(b'ANY', 'Any'), (b'ALL', 'All')])),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AddField(
+ model_name='condition',
+ name='rule',
+ field=models.ForeignKey(to='servo.Rule'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='action',
+ name='rule',
+ field=models.ForeignKey(to='servo.Rule'),
+ preserve_default=True,
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='customer',
+ field=mptt.fields.TreeForeignKey(blank=True, to='servo.Customer', null=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/servo/migrations/0017_auto_20150430_2233.py b/servo/migrations/0017_auto_20150430_2233.py
new file mode 100644
index 0000000..8303e17
--- /dev/null
+++ b/servo/migrations/0017_auto_20150430_2233.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.db.models.manager
+import servo.models.account
+import django.core.validators
+import django.contrib.auth.models
+import servo.defaults
+import django.contrib.sites.managers
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0016_auto_20150316_1152'),
+ ]
+
+ operations = [
+ migrations.AlterModelManagers(
+ name='attachment',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='checklist',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='customer',
+ managers=[
+ (b'objects', django.db.models.manager.Manager()),
+ (b'on_site', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='device',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='event',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='flaggeditem',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='invoice',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='note',
+ managers=[
+ (b'objects', django.db.models.manager.Manager()),
+ (b'on_site', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='order',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='productcategory',
+ managers=[
+ (b'objects', django.db.models.manager.Manager()),
+ (b'on_site', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='queue',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='rateditem',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='tag',
+ managers=[
+ (b'objects', django.db.models.manager.Manager()),
+ (b'on_site', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='taggeditem',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='timeditem',
+ managers=[
+ (b'objects', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AlterModelManagers(
+ name='user',
+ managers=[
+ (b'objects', django.contrib.auth.models.UserManager()),
+ (b'techies', servo.models.account.TechieManager()),
+ (b'active', servo.models.account.ActiveManager()),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='checklist',
+ name='queues',
+ field=models.ManyToManyField(to='servo.Queue', verbose_name='queue', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='key',
+ field=models.CharField(max_length=16, choices=[(b'QUEUE', 'Queue'), (b'STATUS', 'Status'), (b'DEVICE', 'Device name'), (b'CUSTOMER_NAME', 'Customer name')]),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='operator',
+ field=models.CharField(default=b'^%s$', max_length=4, choices=[(b'^%s$', 'Equals'), (b'%s', 'Contains'), (b'%d < %d', 'Less than'), (b'%d > %d', 'Greater than')]),
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='devices',
+ field=models.ManyToManyField(verbose_name='devices', editable=False, to='servo.Device', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='email',
+ field=models.EmailField(default=b'', max_length=254, verbose_name='email', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='groups',
+ field=models.ManyToManyField(to='servo.CustomerGroup', verbose_name='Groups', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='customer',
+ name='tags',
+ field=models.ManyToManyField(to='servo.Tag', verbose_name='tags', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='products',
+ field=models.ManyToManyField(help_text='Products that are compatible with this device instance', to='servo.Product', editable=False),
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='purchase_country',
+ field=models.CharField(default=servo.defaults.country, editable=False, choices=[('AF', 'Afghanistan'), ('AX', '\xc5land Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('CV', 'Cabo Verde'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo (the Democratic Republic of the)'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "C\xf4te d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CW', 'Cura\xe7ao'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands [Malvinas]'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', 'Laos'), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia (Federated States of)'), ('MD', 'Moldovia'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('KP', 'North Korea'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestine, State of'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'R\xe9union'), ('RO', 'Romania'), ('RU', 'Russia'), ('RW', 'Rwanda'), ('BL', 'Saint Barth\xe9lemy'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin (French part)'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SX', 'Sint Maarten (Dutch part)'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('KR', 'South Korea'), ('SS', 'South Sudan'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syria'), ('TW', 'Taiwan'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('UM', 'United States Minor Outlying Islands'), ('US', 'United States of America'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Vietnam'), ('VG', 'Virgin Islands (British)'), ('VI', 'Virgin Islands (U.S.)'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], max_length=128, blank=True, verbose_name='Purchase Country'),
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='email',
+ field=models.EmailField(default=b'', max_length=254, verbose_name='email', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='location',
+ name='gsx_accounts',
+ field=models.ManyToManyField(to='servo.GsxAccount', verbose_name='Accounts', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='note',
+ name='labels',
+ field=models.ManyToManyField(to='servo.Tag', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='devices',
+ field=models.ManyToManyField(to='servo.Device', through='servo.OrderDevice'),
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='state',
+ field=models.IntegerField(default=0, choices=[(0, 'Unassigned'), (1, 'Open'), (2, 'Closed')]),
+ ),
+ migrations.AlterField(
+ model_name='product',
+ name='categories',
+ field=models.ManyToManyField(to='servo.ProductCategory', verbose_name='Categories', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='product',
+ name='device_models',
+ field=models.ManyToManyField(to='servo.DeviceGroup', verbose_name='device models', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='shippingmethod',
+ name='notify_email',
+ field=models.EmailField(max_length=254, null=True, blank=True),
+ ),
+ migrations.AlterField(
+ model_name='status',
+ name='queue',
+ field=models.ManyToManyField(to='servo.Queue', editable=False, through='servo.QueueStatus'),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='email',
+ field=models.EmailField(max_length=254, verbose_name='email address', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='groups',
+ field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='last_login',
+ field=models.DateTimeField(null=True, verbose_name='last login', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='locations',
+ field=models.ManyToManyField(to='servo.Location', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='queues',
+ field=models.ManyToManyField(to='servo.Queue', verbose_name='queues', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='username',
+ field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username'),
+ ),
+ ]
diff --git a/servo/migrations/0018_auto_20150505_0020.py b/servo/migrations/0018_auto_20150505_0020.py
new file mode 100644
index 0000000..b16f130
--- /dev/null
+++ b/servo/migrations/0018_auto_20150505_0020.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import servo.models.note
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0017_auto_20150430_2233'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='note',
+ name='escalation',
+ field=servo.models.note.UnsavedForeignKey(editable=False, to='servo.Escalation', null=True),
+ ),
+ ]
diff --git a/servo/migrations/0019_auto_20150507_0048.py b/servo/migrations/0019_auto_20150507_0048.py
new file mode 100644
index 0000000..7314a55
--- /dev/null
+++ b/servo/migrations/0019_auto_20150507_0048.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0018_auto_20150505_0020'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='order',
+ options={'ordering': ('-priority', 'id'), 'permissions': (('change_user', 'Can set assignee'), ('change_status', 'Can change status'), ('follow_order', 'Can follow order'), ('copy_order', 'Can copy order'), ('batch_process', 'Can batch process'))},
+ ),
+ ]
diff --git a/servo/migrations/0020_auto_20150514_0618.py b/servo/migrations/0020_auto_20150514_0618.py
new file mode 100644
index 0000000..1ae0627
--- /dev/null
+++ b/servo/migrations/0020_auto_20150514_0618.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0019_auto_20150507_0048'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='location',
+ name='timezone',
+ field=models.CharField(default=b'UTC', max_length=128, verbose_name='Time zone', choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Santa_Isabel', b'America/Santa_Isabel'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Rangoon', b'Asia/Rangoon'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GMT', b'GMT'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Johnston', b'Pacific/Johnston'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'US/Alaska', b'US/Alaska'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'UTC', b'UTC')]),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='timezone',
+ field=models.CharField(default=b'UTC', help_text='Your current timezone', max_length=128, verbose_name='Time zone', choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Santa_Isabel', b'America/Santa_Isabel'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Rangoon', b'Asia/Rangoon'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GMT', b'GMT'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Johnston', b'Pacific/Johnston'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'US/Alaska', b'US/Alaska'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'UTC', b'UTC')]),
+ ),
+ ]
diff --git a/servo/migrations/0021_auto_20150608_1703.py b/servo/migrations/0021_auto_20150608_1703.py
new file mode 100644
index 0000000..45981d4
--- /dev/null
+++ b/servo/migrations/0021_auto_20150608_1703.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0020_auto_20150514_0618'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='device',
+ name='password',
+ field=models.CharField(default=b'', max_length=32, verbose_name='password'),
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='purchased_on',
+ field=models.DateField(null=True, verbose_name='Date of Purchase', blank=True),
+ ),
+ ]
diff --git a/servo/migrations/0022_auto_20150612_0803.py b/servo/migrations/0022_auto_20150612_0803.py
new file mode 100644
index 0000000..6a3b90c
--- /dev/null
+++ b/servo/migrations/0022_auto_20150612_0803.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0021_auto_20150608_1703'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='device',
+ name='product_line',
+ field=models.CharField(default=b'OTHER', max_length=16, verbose_name='Product Line', choices=[(b'IPODCLASSIC', b'iPod Classic'), (b'POWERMAC', b'Power Mac'), (b'APPLETV', b'Apple TV'), (b'IMAC', b'iMac'), (b'OTHER', b'Other Products'), (b'MACBOOKAIR', b'MacBook Air'), (b'DISPLAYS', b'Display'), (b'IPODTOUCH', b'iPod Touch'), (b'MACPRO', b'Mac Pro'), (b'IPODNANO', b'iPod nano'), (b'IPAD', b'iPad'), (b'MACBOOK', b'MacBook'), (b'MACACCESSORY', b'Mac Accessory'), (b'MACMINI', b'Mac mini'), (b'BEATS', b'Beats Products'), (b'SERVER', b'Server'), (b'MACBOOKLEGACY', b'MacBook'), (b'IPHONE', b'iPhone'), (b'IPHONEACCESSORY', b'iPhone Accessory'), (b'IPODSHUFFLE', b'iPod Shuffle'), (b'MACBOOKPRO', b'MacBook Pro')]),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='timezone',
+ field=models.CharField(default=b'Europe/Helsinki', help_text='Your current timezone', max_length=128, verbose_name='Time zone', choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Santa_Isabel', b'America/Santa_Isabel'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Rangoon', b'Asia/Rangoon'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GMT', b'GMT'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Johnston', b'Pacific/Johnston'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'US/Alaska', b'US/Alaska'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'UTC', b'UTC')]),
+ ),
+ ]
diff --git a/servo/migrations/0023_auto_20150612_0822.py b/servo/migrations/0023_auto_20150612_0822.py
new file mode 100644
index 0000000..840a1f5
--- /dev/null
+++ b/servo/migrations/0023_auto_20150612_0822.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('servo', '0022_auto_20150612_0803'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='device',
+ name='password',
+ field=models.CharField(default=b'', max_length=32, verbose_name='password', blank=True),
+ ),
+ migrations.AlterField(
+ model_name='device',
+ name='product_line',
+ field=models.CharField(default=b'OTHER', max_length=16, verbose_name='Product Line', choices=[(b'IPODCLASSIC', b'iPod Classic'), (b'POWERMAC', b'Power Mac'), (b'APPLETV', b'Apple TV'), (b'IMAC', b'iMac'), (b'OTHER', b'Other Products'), (b'MACBOOKAIR', b'MacBook Air'), (b'DISPLAYS', b'Display'), (b'IPODTOUCH', b'iPod Touch'), (b'MACPRO', b'Mac Pro'), (b'IPODNANO', b'iPod nano'), (b'IPAD', b'iPad'), (b'MACBOOK', b'MacBook'), (b'MACACCESSORY', b'Mac Accessory'), (b'MACMINI', b'Mac mini'), (b'SERVER', b'Server'), (b'BEATS', b'Beats Products'), (b'IPHONE', b'iPhone'), (b'IPHONEACCESSORY', b'iPhone Accessory'), (b'IPODSHUFFLE', b'iPod Shuffle'), (b'MACBOOKPRO', b'MacBook Pro')]),
+ ),
+ ]
diff --git a/servo/migrations/__init__.py b/servo/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/migrations/__init__.py
diff --git a/servo/models/__init__.py b/servo/models/__init__.py
new file mode 100644
index 0000000..1a109c8
--- /dev/null
+++ b/servo/models/__init__.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from common import *
+from product import *
+from account import *
+from queue import *
+from calendar import *
+from customer import *
+from device import *
+from note import *
+from order import *
+from invoices import *
+from purchases import *
+from shipments import *
+from parts import *
+from repair import *
+from escalations import *
+from rules import *
diff --git a/servo/models/account.py b/servo/models/account.py
new file mode 100644
index 0000000..883a877
--- /dev/null
+++ b/servo/models/account.py
@@ -0,0 +1,318 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import locale
+
+from django.db import models
+from django.conf import settings
+
+from pytz import common_timezones
+from django.core.cache import cache
+from django.core.urlresolvers import reverse
+from rest_framework.authtoken.models import Token
+
+from mptt.fields import TreeForeignKey
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import AbstractUser, Group, UserManager
+
+from servo import defaults
+from servo.models.common import Location, Configuration
+from servo.models.queue import Queue
+from servo.models.customer import Customer
+
+
+class ActiveManager(UserManager):
+ def get_queryset(self):
+ r = super(ActiveManager, self).get_queryset().filter(is_visible=True)
+ return r.filter(is_active=True)
+
+
+class TechieManager(UserManager):
+ def get_queryset(self):
+ return super(TechieManager, self).get_queryset().filter(tech_id__regex=r'\w{8}')
+
+ def active(self):
+ return self.get_queryset().filter(is_active=True)
+
+
+class User(AbstractUser):
+ site = models.ForeignKey(Site, editable=False, default=defaults.site_id)
+ customer = TreeForeignKey(
+ Customer,
+ null=True,
+ blank=True,
+ limit_choices_to={'is_company': True}
+ )
+
+ full_name = models.CharField(
+ max_length=128,
+ editable=False,
+ default=_('New User')
+ )
+
+ locations = models.ManyToManyField(Location, blank=True)
+
+ # The location this user is currently in
+ location = models.ForeignKey(
+ Location,
+ null=True,
+ related_name='+',
+ on_delete=models.PROTECT,
+ verbose_name=_('Current Location'),
+ help_text=_(u'Orders you create will be registered to this location.')
+ )
+ queues = models.ManyToManyField(Queue, blank=True, verbose_name=_('queues'))
+ LOCALES = (
+ ('da_DK.UTF-8', _("Danish")),
+ ('nl_NL.UTF-8', _("Dutch")),
+ ('en_US.UTF-8', _("English")),
+ ('et_EE.UTF-8', _("Estonian")),
+ ('fi_FI.UTF-8', _("Finnish")),
+ ('sv_SE.UTF-8', _("Swedish")),
+ )
+ locale = models.CharField(
+ max_length=32,
+ choices=LOCALES,
+ default=LOCALES[0][0],
+ verbose_name=_('language'),
+ help_text=_("Select which language you want to use Servo in.")
+ )
+
+ TIMEZONES = tuple((t, t) for t in common_timezones)
+ timezone = models.CharField(
+ max_length=128,
+ choices=TIMEZONES,
+ default=settings.TIMEZONE,
+ verbose_name=_('Time zone'),
+ help_text=_("Your current timezone")
+ )
+
+ REGIONS = (
+ ('da_DK.UTF-8', _("Denmark")),
+ ('et_EE.UTF-8', _("Estonia")),
+ ('fi_FI.UTF-8', _("Finland")),
+ ('en_US.UTF-8', _("United States")),
+ ('nl_NL.UTF-8', _("Netherlands")),
+ ('sv_SE.UTF-8', _("Sweden")),
+ )
+ region = models.CharField(
+ max_length=32,
+ choices=REGIONS,
+ default=defaults.locale,
+ verbose_name=_('region'),
+ help_text=_("Affects formatting of numbers, dates and currencies.")
+ )
+ should_notify = models.BooleanField(
+ default=True,
+ verbose_name=_('Enable notifications'),
+ help_text=_("Enable notifications in the toolbar.")
+ )
+ notify_by_email = models.BooleanField(
+ default=False,
+ verbose_name=_('email notifications'),
+ help_text=_("Event notifications will also be emailed to you.")
+ )
+ autoprint = models.BooleanField(
+ default=True,
+ verbose_name=_('print automatically'),
+ help_text=_("Opens print dialog automatically.")
+ )
+ tech_id = models.CharField(
+ blank=True,
+ default='',
+ max_length=16,
+ verbose_name=_("tech ID")
+ )
+ gsx_userid = models.CharField(
+ blank=True,
+ default='',
+ max_length=128,
+ verbose_name=_("User ID")
+ )
+ gsx_password = models.CharField(
+ blank=True,
+ default='',
+ max_length=256,
+ verbose_name=_("Password")
+ )
+ gsx_poprefix = models.CharField(
+ blank=True,
+ default='',
+ max_length=8,
+ verbose_name=_("PO prefix"),
+ help_text=_("GSX repairs you create will be prefixed")
+ )
+
+ photo = models.ImageField(
+ null=True,
+ blank=True,
+ upload_to="avatars",
+ verbose_name=_('photo'),
+ help_text=_("Maximum avatar size is 1MB")
+ )
+
+ is_visible = models.BooleanField(default=True, editable=False)
+
+ objects = UserManager()
+ techies = TechieManager()
+ active = ActiveManager()
+
+ def get_location_list(self):
+ results = []
+ for l in self.locations.all():
+ results.append({'pk': l.pk, 'name': l.title})
+
+ return results
+
+ @classmethod
+ def serialize(cls, queryset):
+ results = []
+ for u in queryset:
+ results.append({'pk': u.pk, 'name': u.get_name()})
+
+ return results
+
+ @classmethod
+ def refresh_nomail(cls):
+ users = cls.active.filter(notify_by_email=False)
+ nomail = [u.email for u in users]
+ cache.set('nomail', nomail)
+
+ @classmethod
+ def get_checkin_group(cls):
+ """
+ Returns all the active members of the check-in group
+ """
+ group = Configuration.conf('checkin_group')
+ return cls.active.filter(groups__pk=group)
+
+ @classmethod
+ def get_checkin_group_list(cls):
+ return cls.serialize(cls.get_checkin_group())
+
+ @classmethod
+ def get_checkin_user(cls):
+ return cls.objects.get(pk=Configuration.conf('checkin_user'))
+
+ def create_token(self):
+ token = Token.objects.create(user=self)
+ return token.key
+
+ def delete_tokens(self):
+ self.get_tokens().delete()
+
+ def get_tokens(self):
+ return Token.objects.filter(user=self)
+
+ def notify(self, msg):
+ pass
+
+ def get_group(self):
+ """
+ Returns the user's primary (first) group
+ """
+ return self.groups.first()
+
+ def get_icon(self):
+ return 'icon-star' if self.is_staff else 'icon-user'
+
+ def get_name(self):
+ return self.full_name if len(self.full_name) > 1 else self.username
+
+ def get_location(self):
+ return self.location
+
+ def get_unread_message_count(self):
+ key = '%s_unread_message_count' % self.user.email
+ count = cache.get(key, 0)
+ return count if count > 0 else ""
+
+ def get_order_count(self, max_state=2):
+ count = self.order_set.filter(state__lt=max_state).count()
+ return count if count > 0 else ""
+
+ def order_count_in_queue(self, queue):
+ count = self.user.order_set.filter(queue=queue).count()
+ return count if count > 0 else ""
+
+ def save(self, *args, **kwargs):
+ self.full_name = u"{0} {1}".format(self.first_name, self.last_name)
+ users = User.objects.filter(notify_by_email=False)
+ nomail = [u.email for u in users]
+ cache.set('nomail', nomail)
+ return super(User, self).save(*args, **kwargs)
+
+ def activate_locale(self):
+ """
+ Activates this user's locale
+ """
+ try:
+ lc = self.locale.split('.')
+ region = self.region.split('.')
+ locale.setlocale(locale.LC_TIME, region)
+ locale.setlocale(locale.LC_MESSAGES, lc)
+ locale.setlocale(locale.LC_NUMERIC, region)
+ locale.setlocale(locale.LC_MONETARY, region)
+ except Exception as e:
+ locale.setlocale(locale.LC_ALL, None)
+
+ # Return the language code
+ return self.locale.split('_', 1)[0]
+
+ def get_avatar(self):
+ try:
+ return self.photo.url
+ except ValueError:
+ return "/static/images/avatar.png"
+
+ def get_admin_url(self):
+ return reverse('admin-edit_user', args=[self.pk])
+
+ def __unicode__(self):
+ return self.get_name() or self.username
+
+ class Meta:
+ app_label = "servo"
+ ordering = ("full_name",)
+ verbose_name = _('User')
+ verbose_name_plural = _('Users & Groups')
+
+
+class UserGroup(Group):
+ site = models.ForeignKey(Site, editable=False, default=defaults.site_id)
+
+ def members_as_list(self):
+ pass
+
+ def get_name(self):
+ return self.name
+
+ def get_admin_url(self):
+ return reverse('admin-edit_group', args=[self.pk])
+
+ class Meta:
+ app_label = 'servo'
diff --git a/servo/models/calendar.py b/servo/models/calendar.py
new file mode 100644
index 0000000..1a8b4e8
--- /dev/null
+++ b/servo/models/calendar.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import math
+
+from dateutil.rrule import DAILY, rrule
+
+from django import forms
+from django.db import models
+from django.conf import settings
+from django.utils import timezone
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.db.models import Sum
+
+
+class Calendar(models.Model):
+
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ editable=False
+ )
+
+ title = models.CharField(
+ max_length=128,
+ verbose_name=_('title'),
+ default=_('New Calendar')
+ )
+ hours_per_day = models.FloatField(
+ null=True,
+ blank=True,
+ verbose_name=_("hours per day"),
+ help_text=_("How many hours per day should be in this calendar")
+ )
+
+ def min_hours(self):
+ return self.hours_per_day or 0
+
+ def get_overtime(self, total_hours, workdays):
+ overtime = total_hours - (self.min_hours() * workdays)
+ return overtime if overtime > 0 else 0
+
+ def subtitle(self, start_date, end_date):
+ workdays = self.get_workdays(start_date, end_date)
+ total_hours = self.get_total_hours(start_date, end_date)
+ overtime = self.get_overtime(total_hours, workdays)
+
+ if overtime > 1:
+ d = {'hours': total_hours, 'workdays': workdays, 'overtime': overtime}
+ subtitle = _("%(hours)s hours total in %(workdays)s days (%(overtime)s hours overtime)." % d)
+ else:
+ d = {'hours': total_hours, 'workdays': workdays}
+ subtitle = _("%(hours)s hours total in %(workdays)s days." % d)
+
+ return subtitle
+
+ def get_workdays(self, start_date, end_date):
+ WORKDAYS = xrange(0, 5)
+ r = rrule(DAILY, dtstart=start_date, until=end_date, byweekday=WORKDAYS)
+ return r.count()
+
+ def get_unfinished_count(self):
+ count = self.calendarevent_set.filter(finished_at=None).count()
+ return count or ""
+
+ def get_total_hours(self, start=None, finish=None):
+ """
+ Returns in hours, the total duration of events in this calendar within
+ a time period.
+ """
+ events = self.calendarevent_set.all()
+
+ if start and finish:
+ events = self.calendarevent_set.filter(started_at__range=(start, finish))
+
+ total = events.aggregate(total=Sum('seconds'))['total'] or 0
+
+ return math.ceil(total/3600.0)
+
+ def get_absolute_url(self):
+ return reverse('calendars.view', args=[self.user.username, self.pk])
+
+ class Meta:
+ app_label = "servo"
+
+
+class CalendarEvent(models.Model):
+
+ calendar = models.ForeignKey(
+ Calendar,
+ editable=False
+ )
+
+ started_at = models.DateTimeField(default=timezone.now)
+ finished_at = models.DateTimeField(null=True, blank=True)
+
+ # The duration of this event in seconds
+ seconds = models.PositiveIntegerField(
+ null=True,
+ editable=False
+ )
+
+ notes = models.TextField(null=True, blank=True)
+
+ def get_start_date(self):
+ return self.started_at.strftime('%x')
+
+ def get_start_time(self):
+ return self.started_at.strftime('%H:%M')
+
+ def get_finish_time(self):
+ try:
+ return self.finished_at.strftime('%H:%M')
+ except AttributeError:
+ return ''
+
+ def set_finished(self, ts=timezone.now):
+ self.finished_at = ts()
+ self.save()
+
+ def get_hours(self):
+ return self.seconds/3600.0
+
+ def get_duration(self):
+ if self.finished_at is None:
+ return ''
+
+ delta = (self.finished_at - self.started_at)
+ hours, remainder = divmod(delta.seconds, 3600)
+ minutes, seconds = divmod(remainder, 60)
+
+ return '%d:%d' % (hours, minutes)
+
+ def get_absolute_url(self):
+ return '/%s/calendars/%d/events/%d' % (self.calendar.user.username, self.calendar.pk, self.pk)
+
+ def save(self, *args, **kwargs):
+ self.seconds = 0
+
+ if self.finished_at:
+ delta = self.finished_at - self.started_at
+ self.seconds = delta.seconds
+
+ super(CalendarEvent, self).save(*args, **kwargs)
+
+ class Meta:
+ app_label = 'servo'
+ ordering = ['-started_at']
+
+
+class CalendarForm(forms.ModelForm):
+ class Meta:
+ model = Calendar
+ exclude = []
+
+
+class CalendarEventForm(forms.ModelForm):
+ class Meta:
+ model = CalendarEvent
+ exclude = []
+ \ No newline at end of file
diff --git a/servo/models/common.py b/servo/models/common.py
new file mode 100644
index 0000000..58b7427
--- /dev/null
+++ b/servo/models/common.py
@@ -0,0 +1,823 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import gsxws
+import os.path
+
+from decimal import Decimal
+from django.core.urlresolvers import reverse
+from django.template.defaultfilters import slugify
+from pytz import common_timezones, country_timezones
+
+from django.db import models
+from django.conf import settings
+from django.contrib.sites.models import Site
+
+from mptt.managers import TreeManager
+from django.contrib.sites.managers import CurrentSiteManager
+
+from mptt.models import MPTTModel, TreeForeignKey
+from django.utils.translation import ugettext_lazy as _
+
+from django.dispatch import receiver
+from django.db.models.signals import post_save
+
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+
+from django.core.cache import cache
+
+from servo import defaults
+from servo.validators import file_upload_validator
+
+
+# Dict for mapping timezones to countries
+TIMEZONE_COUNTRY = {}
+
+for cc in country_timezones:
+ timezones = country_timezones[cc]
+ for timezone in timezones:
+ TIMEZONE_COUNTRY[timezone] = cc
+
+
+class CsvTable(object):
+ def __init__(self, colwidth=20):
+ self.rowcount = 0
+ self.colwidth = colwidth
+ self.body = u''
+ self.table = u''
+ self.header = u''
+
+ def padrow(self, row):
+ r = []
+ for c in row:
+ r.append(unicode(c).ljust(self.colwidth))
+
+ return r
+
+ def addheader(self, new_header):
+ self.rowcount = self.rowcount + 1
+ header = self.padrow(new_header)
+ self.header = ''.join(header)
+
+ def addrow(self, new_row):
+ row = self.padrow(new_row)
+ self.body += ''.join(row) + "\n"
+
+ def has_body(self):
+ return self.body != ''
+
+ def __unicode__(self):
+ self.table = self.header + "\n" + self.body
+ return self.table
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+
+class BaseItem(models.Model):
+ """
+ Base class for a few generic relationships
+ """
+ site = models.ForeignKey(Site, editable=False, default=defaults.site_id)
+
+ object_id = models.PositiveIntegerField()
+ content_type = models.ForeignKey(ContentType)
+ content_object = GenericForeignKey("content_type", "object_id")
+
+ objects = CurrentSiteManager()
+
+ class Meta:
+ abstract = True
+ app_label = "servo"
+
+
+class RatedItem(BaseItem):
+ rating = models.PositiveIntegerField()
+
+
+class TimedItem(BaseItem):
+ status = models.CharField(max_length=128)
+ started_at = models.DateTimeField()
+ timeout_at = models.DateTimeField()
+
+
+class TaggedItem(BaseItem):
+ """
+ A generic tagged item
+ """
+ tag = models.CharField(max_length=128)
+ slug = models.SlugField()
+ color = models.CharField(max_length=8, default="")
+
+ def save(self, *args, **kwargs):
+ self.slug = slugify(self.tag)
+ super(TaggedItem, self).save(*args, **kwargs)
+
+ def __unicode__(self):
+ return self.tag
+
+ class Meta:
+ app_label = "servo"
+ unique_together = ("content_type", "object_id", "tag",)
+
+
+class FlaggedItem(BaseItem):
+ flagged_by = models.ForeignKey(settings.AUTH_USER_MODEL)
+
+
+class Event(BaseItem):
+ """
+ Something that happens
+ """
+ description = models.CharField(max_length=255)
+
+ triggered_by = models.ForeignKey(settings.AUTH_USER_MODEL)
+ triggered_at = models.DateTimeField(auto_now_add=True)
+ handled_at = models.DateTimeField(null=True)
+
+ action = models.CharField(max_length=32)
+ priority = models.SmallIntegerField(default=1)
+
+ notify_users = models.ManyToManyField(
+ settings.AUTH_USER_MODEL,
+ related_name="notifications" # request.user.notifications
+ )
+
+ def save(self, *args, **kwargs):
+ saved = super(Event, self).save(*args, **kwargs)
+
+ if settings.ENABLE_RULES:
+ from servo.tasks import apply_rules
+ apply_rules.delay(self)
+
+ def get_status(self):
+ from servo.models import Status
+ return Status.objects.get(title=self.description)
+
+ def get_icon(self):
+ return "events/%s-%s" % (self.content_type, self.action)
+
+ def get_link(self):
+ return self.content_object.get_absolute_url()
+
+ def get_class(self):
+ return "disabled" if self.handled_at else ""
+
+ def __unicode__(self):
+ return self.description
+
+ class Meta:
+ ordering = ('priority', '-id',)
+ app_label = "servo"
+
+
+class GsxAccount(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ title = models.CharField(max_length=128, default=_("New GSX Account"))
+ sold_to = models.CharField(max_length=10, verbose_name=_("Sold-To"))
+ ship_to = models.CharField(max_length=10, verbose_name=_("Ship-To"))
+
+ region = models.CharField(
+ max_length=3,
+ choices=gsxws.GSX_REGIONS,
+ verbose_name=_("Region")
+ )
+
+ user_id = models.CharField(
+ blank=True,
+ default='',
+ max_length=128,
+ verbose_name=_("User ID")
+ )
+
+ password = models.CharField(
+ blank=True,
+ default='',
+ max_length=256,
+ verbose_name=_("Password")
+ )
+
+ environment = models.CharField(
+ max_length=2,
+ verbose_name=_("Environment"),
+ choices=gsxws.ENVIRONMENTS,
+ default=gsxws.ENVIRONMENTS[0][0]
+ )
+
+ @classmethod
+ def get_soldto_choices(cls):
+ choices = []
+ for i in cls.objects.all():
+ choice = (i.sold_to, '%s (%s)' % (i.sold_to, i.title))
+ choices.append(choice)
+
+ choices = [('', '------------------'),] + choices
+ return choices
+
+ @classmethod
+ def get_shipto_choices(cls):
+ return cls.objects.values_list('ship_to', 'ship_to')
+
+
+ @classmethod
+ def get_default_account(cls):
+ act_pk = Configuration.conf('gsx_account')
+
+ if act_pk in ('', None,):
+ raise ValueError(_('Default GSX account not configured'))
+
+ return GsxAccount.objects.get(pk=act_pk)
+
+ @classmethod
+ def get_account(cls, location, queue=None):
+ """
+ Returns the correct GSX account for the specified user/queue
+ """
+ try:
+ act = location.gsx_accounts.get(sold_to=queue.gsx_soldto)
+ except Exception as e:
+ act = GsxAccount.get_default_account()
+
+ return act
+
+ @classmethod
+ def default(cls, user, queue=None):
+ """
+ Returns the correct GSX account for
+ the specified user/queue and connects to it
+ """
+ try:
+ act = GsxAccount.get_account(user.location, queue)
+ except ValueError:
+ raise gsxws.GsxError(_('Configuration error'))
+
+ return act.connect(user)
+
+ def connect(self, user, location=None):
+ """
+ Connects to this GSX Account
+ """
+ if user.gsx_userid:
+ self.user_id = user.gsx_userid
+ if user.gsx_password:
+ self.password = user.gsx_password
+
+ if location is None:
+ timezone = user.location.gsx_tz
+ else:
+ timezone = location.gsx_tz
+
+ gsxws.connect(user_id=self.user_id,
+ password=self.password,
+ sold_to=self.sold_to,
+ environment=self.environment,
+ timezone=timezone)
+ return self
+
+ def test(self):
+ """
+ Tests that the account details are correct
+ """
+ if self.user_id and self.password:
+ gsxws.connect(sold_to=self.sold_to,
+ user_id=self.user_id,
+ password=self.password,
+ environment=self.environment)
+
+ def get_admin_url(self):
+ return reverse('admin-edit_gsx_account', args=[self.pk])
+
+ def __unicode__(self):
+ return u"%s (%s)" % (self.title, self.get_environment_display())
+
+ class Meta:
+ app_label = 'servo'
+ get_latest_by = 'id'
+ ordering = ['title']
+ verbose_name = _("GSX Account")
+ verbose_name_plural = _("GSX Accounts")
+ unique_together = ('sold_to', 'ship_to', 'environment', 'site',)
+
+
+class Tag(MPTTModel):
+ """
+ A tag is a simple one-word descriptor for something.
+ The type attribute is used to group tags to make them easier
+ to associate with different elements
+ """
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ title = models.CharField(
+ unique=True,
+ max_length=255,
+ default=_('New Tag'),
+ verbose_name=_('name')
+ )
+
+ TYPES = (
+ ('device', _('Device')),
+ ('order', _('Order')),
+ ('note', _('Note')),
+ ('other', _('Other')),
+ )
+
+ type = models.CharField(
+ max_length=32,
+ choices=TYPES,
+ verbose_name=_(u'type')
+ )
+
+ parent = TreeForeignKey(
+ 'self',
+ null=True,
+ blank=True,
+ related_name='children'
+ )
+
+ times_used = models.IntegerField(default=0, editable=False)
+
+ COLORS = (
+ ('default', _('Default')),
+ ('success', _('Green')),
+ ('warning', _('Orange')),
+ ('important', _('Red')),
+ ('info', _('Blue')),
+ )
+
+ color = models.CharField(
+ max_length=16,
+ blank=True,
+ null=True,
+ choices=COLORS,
+ default='default'
+ )
+
+ def count_open_orders(self):
+ count = self.order_set.filter(state__lt=2).count()
+ return count if count > 0 else ''
+
+ def get_admin_url(self):
+ return reverse('admin-edit_tag', args=[self.type, self.pk])
+
+ def __unicode__(self):
+ return self.title
+
+ objects = TreeManager()
+ on_site = CurrentSiteManager()
+
+ class Meta:
+ app_label = 'servo'
+ verbose_name = _('Tag')
+ verbose_name_plural = _('Tags')
+
+ class MPTTMeta:
+ order_insertion_by = ['title']
+
+
+class Location(models.Model):
+ """
+ A Service Location within a company
+ """
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ title = models.CharField(
+ max_length=255,
+ verbose_name=_(u'name'),
+ default=_('New Location'),
+ )
+ phone = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_('phone')
+ )
+ email = models.EmailField(blank=True, default='', verbose_name=_('email'))
+ address = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_(u'address')
+ )
+ zip_code = models.CharField(
+ blank=True,
+ default='',
+ max_length=8,
+ verbose_name=_(u'ZIP Code')
+ )
+ city = models.CharField(
+ blank=True,
+ default='',
+ max_length=16,
+ verbose_name=_(u'city')
+ )
+
+ TIMEZONES = tuple((t, t) for t in common_timezones)
+
+ timezone = models.CharField(
+ default='UTC',
+ max_length=128,
+ choices=TIMEZONES,
+ verbose_name=_('Time zone')
+ )
+
+ # It would make more sense to just store the Ship-To
+ # per-location, but some location can have multiple Ship-Tos :-/
+ gsx_accounts = models.ManyToManyField(
+ GsxAccount,
+ blank=True,
+ verbose_name=_('Accounts')
+ )
+
+ gsx_shipto = models.CharField(
+ max_length=10,
+ default='',
+ blank=True,
+ verbose_name=_('Ship-To')
+ )
+
+ gsx_tz = models.CharField(
+ max_length=4,
+ default='CEST',
+ verbose_name=_('Timezone'),
+ choices=gsxws.GSX_TIMEZONES
+ )
+
+ notes = models.TextField(
+ blank=True,
+ default='9:00 - 18:00',
+ verbose_name=_('Notes'),
+ help_text=_('Will be shown on print templates')
+ )
+
+ logo = models.FileField(
+ null=True,
+ blank=True,
+ upload_to='logos',
+ verbose_name=_('Logo')
+ )
+
+ enabled = models.BooleanField(
+ default=True,
+ verbose_name=_('Enabled')
+ )
+
+ def get_shipto_choices(self):
+ return self.gsx_accounts.values_list('ship_to', 'ship_to')
+
+ def get_country(self):
+ try:
+ return TIMEZONE_COUNTRY[self.timezone]
+ except KeyError:
+ return 'FI'
+
+ def ship_to_choices(self):
+ choices = []
+ for i in self.gsx_accounts.all():
+ choices.append((i.ship_to, i.ship_to))
+ return choices
+
+ def get_admin_url(self):
+ return reverse('admin-edit_location', args=[self.pk])
+
+ def gsx_address(self):
+ return {
+ 'city': self.city,
+ 'zipCode': self.zip_code,
+ 'country': self.get_country(),
+ 'primaryPhone': self.phone,
+ 'emailAddress': self.email,
+ }
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ ordering = ('title',)
+ app_label = 'servo'
+ get_latest_by = 'id'
+ verbose_name = _('Location')
+ verbose_name_plural = _('Locations')
+ unique_together = ('title', 'site',)
+
+
+class Configuration(models.Model):
+ site = models.ForeignKey(Site, editable=False, default=defaults.site_id)
+ key = models.CharField(max_length=255)
+ value = models.TextField(default='', blank=True)
+
+ @classmethod
+ def true(cls, key):
+ return cls.conf(key) == 'True'
+
+ @classmethod
+ def false(cls, key):
+ return not cls.true(key)
+
+ @classmethod
+ def get_company_logo(cls):
+ return cls.conf('company_logo')
+
+ @classmethod
+ def default_subject(cls):
+ return cls.conf('default_subject')
+
+ @classmethod
+ def get_default_sender(cls, user):
+ conf = cls.conf()
+ sender = conf.get('default_sender')
+
+ if sender == 'user':
+ return user.email
+ if sender == 'location':
+ return user.get_location().email
+
+ return conf.get('default_sender_custom')
+
+ @classmethod
+ def track_inventory(cls):
+ return cls.conf('track_inventory') == 'True'
+
+ @classmethod
+ def notify_location(cls):
+ return cls.conf('notify_location') == 'True'
+
+ @classmethod
+ def notify_email_address(cls):
+ """
+ Returns the email address to send reports to
+ or None if it's invalid
+ """
+ from django.core.validators import validate_email
+ try:
+ validate_email(conf['notify_address'])
+ return conf['notify_address']
+ except Exception:
+ pass
+
+ @classmethod
+ def autocomplete_repairs(cls):
+ return cls.conf('autocomplete_repairs') == 'True'
+
+ @classmethod
+ def smtp_ssl(cls):
+ return cls.conf('smtp_ssl') == 'True'
+
+ @classmethod
+ def get_imap_server(cls):
+ import imaplib
+ conf = cls.conf()
+
+ if not conf.get('imap_host'):
+ raise ValueError("No IMAP server defined - check your configuration")
+
+ if conf.get('imap_ssl'):
+ server = imaplib.IMAP4_SSL(conf['imap_host'])
+ else:
+ server = imaplib.IMAP4(conf['imap_host'])
+
+ server.login(conf['imap_user'], conf['imap_password'])
+ server.select()
+ return server
+
+ @classmethod
+ def conf(cls, key=None):
+ """
+ Returns the admin-configurable config of the site
+ """
+ config = cache.get('config')
+ if config is None:
+ config = dict()
+ for r in Configuration.objects.all():
+ config[r.key] = r.value
+
+ cache.set('config', config)
+
+ return config.get(key) if key else config
+
+ def save(self, *args, **kwargs):
+ config = super(Configuration, self).save(*args, **kwargs)
+ # Using cache instead of session since it's shared among
+ # all the users of the instance
+ cache.set('config', config, 60*60*24*1)
+
+ class Meta:
+ app_label = 'servo'
+ unique_together = ('key', 'site',)
+
+
+class Property(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ TYPES = (
+ ('customer', _('Customer')),
+ ('order', _('Order')),
+ ('product', _('Product'))
+ )
+
+ title = models.CharField(
+ max_length=255,
+ default=_('New Field'),
+ verbose_name=_('title')
+ )
+
+ type = models.CharField(
+ max_length=32,
+ choices=TYPES,
+ default=TYPES[0],
+ verbose_name=_('type')
+ )
+ format = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_('format')
+ )
+ value = models.TextField(blank=True, default='', verbose_name=_('value'))
+
+ def __unicode__(self):
+ return self.title
+
+ def get_admin_url(self):
+ return reverse('admin-edit_field', args=[self.type, self.pk])
+
+ def values(self):
+ if self.value is None:
+ return []
+ else:
+ return self.value.split(', ')
+
+ class Meta:
+ app_label = 'servo'
+ ordering = ['title']
+ verbose_name = _('Field')
+ verbose_name_plural = _('Fields')
+
+
+class Search(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ query = models.TextField()
+ model = models.CharField(max_length=32)
+ title = models.CharField(max_length=128)
+ shared = models.BooleanField(default=True)
+
+ class Meta:
+ app_label = 'servo'
+
+
+class Notification(models.Model):
+ """
+ A notification is a user-configurable response to an event
+ """
+ KINDS = (('order', u'Tilaus'), ('note', u'Merkintä'))
+ ACTIONS = (('created', u'Luotu'), ('edited', u'Muokattu'))
+
+ kind = models.CharField(max_length=16)
+ action = models.CharField(max_length=16)
+ message = models.TextField()
+
+ class Meta:
+ app_label = 'servo'
+
+
+class Template(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ title = models.CharField(
+ blank=False,
+ unique=True,
+ max_length=128,
+ verbose_name=_('title'),
+ default=_('New Template')
+ )
+
+ content = models.TextField(blank=False, verbose_name=_('content'))
+
+ @classmethod
+ def templates(self):
+ choices = Template.objects.all().values_list('title', flat=True)
+ return list(choices)
+
+ def get_absolute_url(self):
+ return reverse('notes-template', args=[self.pk])
+
+ def get_admin_url(self):
+ return reverse('admin-edit_template', args=[self.pk])
+
+ def get_delete_url(self):
+ return reverse('admin-delete_template', args=[self.pk])
+
+ class Meta:
+ ordering = ['title']
+ app_label = "servo"
+ verbose_name = _('Template')
+ verbose_name_plural = _('Templates')
+
+
+class Attachment(BaseItem):
+ """
+ A file attached to something
+ """
+ mime_type = models.CharField(max_length=64, editable=False)
+ content = models.FileField(
+ upload_to='attachments',
+ verbose_name=_('file'),
+ validators=[file_upload_validator]
+ )
+
+ @classmethod
+ def get_content_type(cls, model):
+ return ContentType.objects.get(app_label='servo', model=model)
+
+ @classmethod
+ def from_file(cls, file):
+ """
+ Returns an attachment object from the file data
+ """
+ attachment = cls(content=file)
+ attachment.save()
+
+ def save(self, *args, **kwargs):
+ DENIED_EXTENSIONS = ('.htm', '.html', '.py', '.js',)
+ filename = self.content.name.lower()
+ ext = os.path.splitext(filename)[1]
+
+ if ext in DENIED_EXTENSIONS:
+ raise ValueError(_(u'%s is not of an allowed file type') % filename)
+
+ super(Attachment, self).save(*args, **kwargs)
+
+ def __unicode__(self):
+ return os.path.basename(self.content.name)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def from_url(self, url):
+ pass
+
+ def get_absolute_url(self):
+ return "/files/%d/view" % self.pk
+
+ class Meta:
+ app_label = 'servo'
+ get_latest_by = "id"
+
+
+@receiver(post_save, sender=Attachment)
+def set_mimetype(sender, instance, created, **kwargs):
+ if created:
+ import subprocess
+ path = instance.content.path
+ mimetype = subprocess.check_output(['file', '-b', '--mime-type', path]).strip()
+ instance.mime_type = mimetype
+ instance.save()
diff --git a/servo/models/customer.py b/servo/models/customer.py
new file mode 100644
index 0000000..e89c3dd
--- /dev/null
+++ b/servo/models/customer.py
@@ -0,0 +1,314 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import phonenumbers
+from django.db import models
+from django.conf import settings
+
+from mptt.managers import TreeManager
+from django.contrib.sites.models import Site
+from django.template.defaultfilters import slugify
+from mptt.models import MPTTModel, TreeForeignKey
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.sites.managers import CurrentSiteManager
+
+from pytz import country_names
+
+from servo import defaults
+from servo.models import Tag
+from servo.models.device import Device
+
+
+class CustomerGroup(models.Model):
+ name = models.CharField(
+ unique=True,
+ max_length=255,
+ default=_('New Group'),
+ verbose_name=_('name')
+ )
+
+ slug = models.SlugField(editable=False)
+
+ def save(self, *args, **kwargs):
+ self.slug = slugify(self.name)
+ super(CustomerGroup, self).save()
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ get_latest_by = 'id'
+ app_label = "servo"
+ ordering = ('id',)
+
+
+class Customer(MPTTModel):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ parent = TreeForeignKey(
+ 'self',
+ null=True,
+ blank=True,
+ related_name='contacts',
+ verbose_name=_('company'),
+ limit_choices_to={'is_company': True}
+ )
+ name = models.CharField(
+ max_length=255,
+ verbose_name=_('name'),
+ default=_('New Customer')
+ )
+ fullname = models.CharField(
+ default='',
+ editable=False,
+ max_length=255
+ )
+ phone = models.CharField(
+ default='',
+ blank=True,
+ max_length=32,
+ verbose_name=_('phone')
+ )
+ email = models.EmailField(
+ blank=True,
+ default='',
+ verbose_name=_('email')
+ )
+ street_address = models.CharField(
+ blank=True,
+ default='',
+ max_length=128,
+ verbose_name=_('address')
+ )
+ zip_code = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_('ZIP Code')
+ )
+ city = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_('city')
+ )
+ COUNTRY_CHOICES = [(k, country_names[k]) for k in sorted(country_names)]
+ country = models.CharField(
+ blank=True,
+ max_length=2,
+ verbose_name=_('Country'),
+ default=defaults.country,
+ choices=COUNTRY_CHOICES
+ )
+ photo = models.ImageField(
+ null=True,
+ blank=True,
+ upload_to="photos",
+ verbose_name=_('photo')
+ )
+
+ groups = models.ManyToManyField(
+ CustomerGroup,
+ blank=True,
+ verbose_name=_('Groups')
+ )
+
+ tags = models.ManyToManyField(
+ Tag,
+ blank=True,
+ verbose_name=_('tags'),
+ limit_choices_to={'type': 'customer'}
+ )
+
+ notes = models.TextField(
+ blank=True,
+ default='',
+ verbose_name=_("notes")
+ )
+
+ devices = models.ManyToManyField(
+ Device,
+ blank=True,
+ editable=False,
+ verbose_name=_("devices")
+ )
+
+ created_at = models.DateTimeField(auto_now=True)
+ is_company = models.BooleanField(
+ default=False,
+ verbose_name=_("company"),
+ help_text=_('Companies can contain contacts')
+ )
+
+ objects = TreeManager()
+ on_site = CurrentSiteManager()
+
+ def get_contacts(self):
+ return self.get_descendants(include_self=False)
+
+ def get_phone(self):
+ return phonenumbers.parse(self.phone, self.country)
+
+ def get_standard_phone(self):
+ n = self.get_phone()
+ fmt = phonenumbers.PhoneNumberFormat.E164
+ return phonenumbers.format_number(n, fmt)
+
+ def get_international_phone(self):
+ n = self.get_phone()
+ fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
+ return phonenumbers.format_number(n, fmt)
+
+ def get_national_phone(self):
+ n = self.get_phone()
+ fmt = phonenumbers.PhoneNumberFormat.NATIONAL
+ return phonenumbers.format_number(n, fmt)
+
+ def get_email_address(self):
+ return '%s <%s>' % (self.name, self.email)
+
+ def get_closest_prop(self, prop):
+ """
+ Gets the 'closest' value of a property
+ """
+ ancestors = self.get_ancestors(ascending=True, include_self=True)
+ for a in ancestors:
+ attr = getattr(a, prop)
+ if attr:
+ return attr
+
+ def gsx_address(self, location):
+ """
+ Returns a dictionary that's compatibly with GSX's Address datatype
+ """
+ out = dict()
+
+ out['country'] = location.get_country()
+ out['city'] = self.get_closest_prop('city') or location.city
+ out['zipCode'] = self.get_closest_prop('zip_code') or location.zip_code
+ out['primaryPhone'] = self.get_closest_prop('phone') or location.phone
+ out['emailAddress'] = self.get_closest_prop('email') or u'refused@apple.com'
+ out['addressLine1'] = self.get_closest_prop('street_address') or location.address
+
+ try:
+ (out['firstName'], out['lastName']) = self.name.split(" ", 1)
+ except Exception:
+ out['firstName'], out['lastName'] = self.name, self.name
+
+ return out
+
+ def get_property(self, key):
+ """
+ Returns the value of a specific property
+ """
+ result = None
+ ci = ContactInfo.objects.filter(customer=self)
+ for i in ci:
+ if i.key == key:
+ result = i.value
+
+ return result
+
+ @property
+ def firstname(self):
+ return self.name.split(" ")[0]
+
+ @property
+ def lastname(self):
+ return self.name.split(" ")[1].rstrip(',')
+
+ def get_fullname(self):
+ """
+ Gets the entire name tree for this customer
+ """
+ title = list()
+
+ for a in self.get_ancestors():
+ title.append(a.name)
+
+ if len(title) < 1:
+ return self.name
+
+ return self.name + " - " + str(", ").join(title)
+
+ def fullprops(self):
+ """
+ Get the combined view of all the properties for this customer
+ """
+ props = {}
+ for r in self.contactinfo_set.all():
+ props[r.key] = r.value
+
+ return props
+
+ def get_group(self):
+ try:
+ return self.groups.latest('id').slug
+ except CustomerGroup.DoesNotExist:
+ return "all"
+
+ def get_absolute_url(self):
+ return "/customers/%s/%d/" % (self.get_group(), self.pk)
+
+ def get_icon(self):
+ return 'icon-briefcase' if self.is_company else 'icon-user'
+
+ def save(self, *args, **kwargs):
+ self.zip_code = self.zip_code.replace(' ', '')
+
+ super(Customer, self).save(*args, **kwargs)
+ fn = self.get_fullname()
+
+ if self.fullname != fn:
+ self.fullname = fn
+ self.save()
+
+ for o in self.orders.all():
+ o.customer_name = fn
+ o.save()
+
+ class Meta:
+ app_label = "servo"
+
+ class MPTTMeta:
+ order_insertion_by = ['name']
+
+ def __unicode__(self):
+ return self.name
+
+
+class ContactInfo(models.Model):
+ customer = models.ForeignKey(Customer)
+ key = models.CharField(max_length=255)
+ value = models.CharField(max_length=255)
+
+ class Meta:
+ app_label = 'servo'
+ # Only allow a field once per customer
+ unique_together = ('customer', 'key',)
diff --git a/servo/models/device.py b/servo/models/device.py
new file mode 100644
index 0000000..ea719a3
--- /dev/null
+++ b/servo/models/device.py
@@ -0,0 +1,523 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import gsxws
+from os.path import basename
+from django_countries import countries
+from django.core.validators import RegexValidator
+
+from django.db import models
+from django.conf import settings
+from django.core.files import File
+from django.core.cache import cache
+from django.dispatch import receiver
+from django.utils.text import slugify
+from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse
+from django.db.models.signals import post_save
+
+from django.contrib.contenttypes.fields import GenericRelation
+
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.managers import CurrentSiteManager
+
+from servo import defaults
+from servo.validators import sn_validator
+from servo.models import GsxAccount, Product, DeviceGroup, TaggedItem
+
+
+class Device(models.Model):
+ """
+ The serviceable device
+ """
+ site = models.ForeignKey(Site, editable=False, default=defaults.site_id)
+
+ # @TODO: unique=True would be nice, but complicated...
+ sn = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_("Serial Number"),
+ validators=[sn_validator]
+ )
+ description = models.CharField(
+ max_length=128,
+ default=_("New Device"),
+ verbose_name=_("description")
+ )
+ brand = models.CharField(
+ blank=True,
+ max_length=128,
+ default=_("Apple"),
+ verbose_name=_("Brand")
+ )
+ reseller = models.CharField(
+ blank=True,
+ default='',
+ max_length=128,
+ verbose_name=_("Reseller")
+ )
+ created_at = models.DateTimeField(auto_now_add=True, null=True)
+ imei = models.CharField(
+ blank=True,
+ default='',
+ max_length=15,
+ verbose_name=_("IMEI Number")
+ )
+ initial_activation_policy = models.CharField(
+ default='',
+ editable=False,
+ max_length=128,
+ verbose_name=_("Initial Activation Policy")
+ )
+ applied_activation_policy = models.CharField(
+ default='',
+ editable=False,
+ max_length=128,
+ verbose_name=_("Applied Activation Policy")
+ )
+ next_tether_policy = models.CharField(
+ default='',
+ editable=False,
+ max_length=128,
+ verbose_name=_("Next Tether Policy")
+ )
+ unlocked = models.NullBooleanField(default=None, editable=False)
+ slug = models.SlugField(null=True, editable=False, max_length=128)
+ PRODUCT_LINES = gsxws.products.models()
+ LINE_CHOICES = [(k, x['name']) for k, x in PRODUCT_LINES.items()]
+ product_line = models.CharField(
+ max_length=16,
+ default="OTHER",
+ choices=LINE_CHOICES,
+ verbose_name=_("Product Line")
+ )
+ products = models.ManyToManyField(
+ Product,
+ editable=False,
+ help_text=_('Products that are compatible with this device instance')
+ )
+ config_code = models.CharField(default='', max_length=8, editable=False)
+ configuration = models.CharField(
+ blank=True,
+ default='',
+ max_length=256,
+ verbose_name=_("configuration")
+ )
+
+ WARRANTY_CHOICES = (
+ ('QP', _("Quality Program")),
+ ('CS', _("Customer Satisfaction")),
+ ('ALW', _("Apple Limited Warranty")),
+ ('APP', _("AppleCare Protection Plan")),
+ ('CC', _("Custom Bid Contracts")),
+ ('CBC', _("Custom Bid Contracts")), # sometimes CC, sometimes CBC?
+ ('WTY', _("3'rd Party Warranty")),
+ ('OOW', _("Out Of Warranty (No Coverage)")),
+ ('NA', _("Unknown")),
+ )
+
+ warranty_status = models.CharField(
+ max_length=3,
+ default="NA",
+ choices=WARRANTY_CHOICES,
+ verbose_name=_("Warranty Status")
+ )
+ username = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_("username")
+ )
+ password = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_("password")
+ )
+ purchased_on = models.DateField(
+ null=True,
+ blank=True,
+ verbose_name=_("Date of Purchase")
+ )
+ purchase_country = models.CharField(
+ blank=True,
+ editable=False,
+ max_length=128,
+ choices=countries,
+ default=defaults.country,
+ verbose_name=_("Purchase Country")
+ )
+
+ sla_description = models.TextField(null=True, editable=False)
+ has_onsite = models.BooleanField(
+ default=False,
+ help_text=_('Device is eligible for onsite repairs in GSX')
+ )
+ contract_start_date = models.DateField(null=True, editable=False)
+ contract_end_date = models.DateField(null=True, editable=False)
+ onsite_start_date = models.DateField(null=True, editable=False)
+ onsite_end_date = models.DateField(null=True, editable=False)
+
+ parts_and_labor_covered = models.BooleanField(default=False, editable=False)
+
+ notes = models.TextField(blank=True, default="", verbose_name=_("notes"))
+ tags = GenericRelation(TaggedItem)
+ photo = models.ImageField(
+ null=True,
+ blank=True,
+ upload_to="devices",
+ verbose_name=_("photo")
+ )
+
+ image_url = models.URLField(
+ null=True,
+ blank=True,
+ verbose_name=_("Image URL")
+ )
+ manual_url = models.URLField(
+ null=True,
+ blank=True,
+ verbose_name=_("Manual URL")
+ )
+ exploded_view_url = models.URLField(
+ null=True,
+ blank=True,
+ verbose_name=_("Exploded View")
+ )
+
+ is_vintage = models.BooleanField(
+ default=False,
+ verbose_name='vintage',
+ help_text=_('Device is considered vintage in GSX')
+ )
+ fmip_active = models.BooleanField(default=False, editable=False)
+
+ objects = CurrentSiteManager()
+
+ def is_apple_device(self):
+ """
+ Checks if this is a valid Apple device SN
+ """
+ valid_sn = gsxws.core.validate(self.sn, 'serialNumber')
+ valid_imei = gsxws.core.validate(self.imei, 'alternateDeviceId')
+ return valid_sn or valid_imei
+
+ def get_sn(self):
+ return self.sn or self.imei
+
+ @property
+ def has_warranty(self):
+ return self.warranty_status in ('ALW', 'APP', 'CBC')
+
+ @property
+ def tag_choices(self):
+ return TaggedItem.objects.filter(content_type__model="device").distinct("tag")
+
+ def add_tags(self, tags):
+ tags = [x for x in tags if x != ''] # Filter out empty tags
+
+ if not tags:
+ return
+
+ content_type = ContentType.objects.get_for_model(Device)
+
+ for t in tags:
+ tag, created = TaggedItem.objects.get_or_create(content_type=content_type,
+ object_id=self.pk,
+ tag=t)
+ tag.save()
+
+ def get_icon(self):
+ if re.match('iPad', self.description):
+ return "ipad"
+ if re.match('iPhone', self.description):
+ return "iphone"
+ if re.match('iPod shuffle', self.description):
+ return "ipod_shuffle"
+ if re.match('iPod', self.description):
+ return "ipod"
+ if re.match('MacBook', self.description):
+ return "macbook"
+
+ return "imac"
+
+ def set_wty_status(self, status):
+ """
+ Translates a GSX warranty status description
+ to our internal representation
+ """
+ if not isinstance(status, basestring):
+ return
+ if re.match(r"Apple Limited", status):
+ self.warranty_status = "ALW"
+ if re.match(r"AppleCare", status):
+ self.warranty_status = "APP"
+ if re.match(r"Customer Satisfaction", status):
+ self.warranty_status = "CSC"
+ if re.match(r"Custom Bid", status):
+ self.warranty_status = "CBC"
+ if re.match(r"Out Of", status):
+ self.warranty_status = "OOW"
+
+ def to_dict(self):
+ result = {'sn': self.sn}
+ result['description'] = self.description
+ result['warranty_status'] = self.warranty_status
+ result['purchased_on'] = self.purchased_on
+ result['purchase_country'] = self.purchase_country
+ result['username'] = self.username
+ result['password'] = self.password
+ return result
+
+ @classmethod
+ def from_dict(cls, d):
+ if d.get('_pk'):
+ return cls.objects.get(pk=d['_pk'])
+
+ device = Device()
+
+ for k, v in d:
+ if k.startswith('_'):
+ continue
+ setattr(device, k, v)
+
+ return device
+
+ def to_gsx(self):
+ if len(self.imei):
+ return gsxws.Product(self.imei)
+ return gsxws.Product(self.sn)
+
+ @classmethod
+ def from_gsx(cls, sn, device=None, cached=True):
+ """
+ Initialize new Device with warranty info from GSX
+ Or update existing one
+ """
+ sn = sn.upper()
+ cache_key = 'device-%s' % sn
+
+ # Only cache unsaved devices
+ if cached and device is None:
+ if cache.get(cache_key):
+ return cache.get(cache_key)
+
+ arg = gsxws.validate(sn)
+
+ if arg not in ("serialNumber", "alternateDeviceId",):
+ raise ValueError(_(u"Invalid input for warranty check: %s") % sn)
+
+ product = gsxws.Product(sn)
+ wty = product.warranty()
+ model = product.model()
+
+ if device is None:
+ # serialNumber may sometimes come back empty
+ serial_number = wty.serialNumber or sn
+ device = Device(sn=serial_number)
+
+ if device.notes == '':
+ device.notes = wty.notes or ''
+ device.notes += wty.csNotes or ''
+
+ device.has_onsite = product.has_onsite
+ device.is_vintage = product.is_vintage
+ device.description = product.description
+ device.fmip_active = product.fmip_is_active
+
+ device.slug = slugify(device.description)
+ device.configuration = wty.configDescription or ''
+ device.purchase_country = wty.purchaseCountry or ''
+
+ device.config_code = model.configCode
+ device.product_line = model.productLine.replace(" ", "")
+ device.parts_and_labor_covered = product.parts_and_labor_covered
+
+ device.sla_description = wty.slaGroupDescription or ''
+ device.contract_start_date = wty.contractCoverageStartDate
+ device.contract_end_date = wty.contractCoverageEndDate
+ device.onsite_start_date = wty.onsiteStartDate
+ device.onsite_end_date = wty.onsiteEndDate
+
+ if wty.estimatedPurchaseDate:
+ device.purchased_on = wty.estimatedPurchaseDate
+
+ device.image_url = wty.imageURL or ''
+ device.manual_url = wty.manualURL or ''
+ device.exploded_view_url = wty.explodedViewURL or ''
+
+ if wty.warrantyStatus:
+ device.set_wty_status(wty.warrantyStatus)
+
+ if product.is_ios:
+ ad = device.get_activation()
+ device.imei = ad.imeiNumber or ''
+ device.unlocked = product.is_unlocked(ad)
+ device.applied_activation_policy = ad.appliedActivationDetails or ''
+ device.initial_activation_policy = ad.initialActivationPolicyDetails or ''
+ device.next_tether_policy = ad.nextTetherPolicyDetails or ''
+
+ cache.set(cache_key, device)
+
+ return device
+
+ def is_mac(self):
+ """
+ Returns True if this is a Mac
+ """
+ p = gsxws.Product(self.sn)
+ p.description = self.description
+ return p.is_mac
+
+ def is_ios(self):
+ """
+ Returns True if this is an iOS device
+ """
+ p = gsxws.Product(self.sn)
+ p.description = self.description
+ return p.is_ios
+
+ def update_gsx_details(self):
+ Device.from_gsx(self.sn, self)
+ self.save()
+
+ def get_image_url(self):
+ url = 'https://static.servoapp.com/images/products/%s.jpg' % self.slug
+ return self.image_url or url
+
+ def get_photo(self):
+ try:
+ return self.photo.url
+ except ValueError:
+ return self.get_image_url()
+
+ def get_fmip_status(self):
+ """
+ Returns the translated FMiP status
+ """
+ return _('Active') if self.fmip_active else _('Inactive')
+
+ def get_coverage_details(self):
+ details = []
+ if self.sla_description:
+ details.append(_(u'SLA Group: %s') % self.sla_description)
+ if self.has_onsite:
+ details.append(_('This unit is eligible for Onsite Service.'))
+ if self.parts_and_labor_covered:
+ details.append(_('Parts and Labor are covered.'))
+
+ return details
+
+ @property
+ def can_create_carryin(self):
+ if self.description == "Non-Serialized Products":
+ # Non-serialized products may have more than one repair
+ return True
+
+ return self.repair_set.filter(completed_at=None).count() < 1
+
+ def get_accessories(self, order):
+ return self.accessory_set.filter(order=order).values_list('name', flat=True)
+
+ def get_activation(self):
+ return gsxws.Product(self.sn).activation()
+
+ def get_diagnostics(self, user):
+ """
+ Fetch GSX iOS or Repair diagnostics based on device type
+ """
+ GsxAccount.default(user)
+ return self.to_gsx().diagnostics()
+
+ def get_warranty(self):
+ return gsxws.Product(self.sn).warranty()
+
+ def get_repairs(self):
+ return gsxws.Product(self.sn).repairs()
+
+ def get_parts(self):
+ """
+ Returns GSX parts for a product with this device's serialNumber
+ """
+ results = {}
+ cache_key = "%s_parts" % self.sn
+
+ for p in gsxws.Product(self.sn).parts():
+ product = Product.from_gsx(p)
+ results[product.code] = product
+
+ cache.set_many(results)
+ cache.set(cache_key, results.values())
+
+ return results.values()
+
+ def import_parts(self):
+ pass
+
+ def save(self, *args, **kwargs):
+ if self.sn:
+ self.sn = self.sn.strip().upper()
+
+ self.description = self.description.strip()
+ if self.slug is None:
+ self.slug = slugify(self.description)
+
+ return super(Device, self).save(*args, **kwargs)
+
+ def get_absolute_url(self):
+ return reverse('devices-view_device', args=[self.product_line, self.slug, self.pk])
+
+ def get_purchase_country(self):
+ # Return device's purchase country, can be 2-letter code (from checkin) or
+ # full country name (from GSX)
+ from django_countries import countries
+
+ if len(self.purchase_country) > 2:
+ return self.purchase_country
+
+ return countries.countries.get(self.purchase_country, '')
+
+ def __unicode__(self):
+ return '%s (%s)' % (self.description, self.sn)
+
+ class Meta:
+ app_label = "servo"
+ get_latest_by = "id"
+
+
+@receiver(post_save, sender=Device)
+def device_saved(sender, instance, created, **kwargs):
+ # make sure we have this tag and product category
+ if created:
+ DeviceGroup.objects.get_or_create(title=instance.description)
+
+ # Update order descriptions
+ for o in instance.order_set.all():
+ o.description = instance.description
+ o.save()
diff --git a/servo/models/escalations.py b/servo/models/escalations.py
new file mode 100644
index 0000000..f937946
--- /dev/null
+++ b/servo/models/escalations.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+import gsxws
+from gsxws.escalations import Context
+
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+from servo import defaults
+from servo.models import GsxAccount, User, Attachment
+
+
+class Escalation(models.Model):
+ """
+ Escalation/Note
+ """
+ escalation_id = models.CharField(
+ default='',
+ max_length=22,
+ editable=False
+ )
+ gsx_account = models.ForeignKey(
+ GsxAccount,
+ default=defaults.gsx_account,
+ verbose_name=_('GSX Account'),
+ )
+ contexts = models.TextField(default='{}', blank=True)
+ issue_type = models.CharField(
+ default='',
+ blank=True,
+ max_length=4,
+ choices=gsxws.escalations.ISSUE_TYPES
+ )
+ status = models.CharField(
+ max_length=1,
+ choices=gsxws.escalations.STATUSES,
+ default=gsxws.escalations.STATUS_OPEN
+ )
+ submitted_at = models.DateTimeField(null=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ created_by = models.ForeignKey(User, editable=False, null=True)
+
+ def is_submitted(self):
+ return self.submitted_at is not None
+
+ def to_gsx(self):
+ self.gsx_account.connect(self.created_by)
+ esc = gsxws.escalations.Escalation()
+
+ note = self.note_set.latest()
+ esc.notes = note.body
+
+ try:
+ attachment = note.attachments.latest()
+ f = attachment.content.file.name
+ a = gsxws.escalations.FileAttachment(f)
+ esc.attachment = a
+ except Attachment.DoesNotExist:
+ pass
+
+ return esc
+
+ def get_escalation(self):
+ esc = gsxws.escalations.Escalation()
+ esc.escalationId = self.escalation_id
+ return esc
+
+ def update(self, note):
+ esc = self.to_gsx()
+ esc.escalationId = self.escalation_id
+ esc.status = self.status
+
+ return esc.update()
+
+ def submit(self):
+ esc = self.to_gsx()
+ esc.shipTo = self.gsx_account.ship_to
+ esc.issueTypeCode = self.issue_type
+
+ if len(self.contexts) > 2:
+ ec = []
+ for k, v in json.loads(self.contexts).items():
+ ec.append(Context(k, v))
+
+ esc.escalationContext = ec
+
+ result = esc.create()
+ self.submitted_at = timezone.now()
+ self.escalation_id = result.escalationId
+
+ self.save()
+
+ @property
+ def subject(self):
+ return _(u'Escalation %s') % self.escalation_id
+
+ class Meta:
+ app_label = "servo"
+
diff --git a/servo/models/invoices.py b/servo/models/invoices.py
new file mode 100644
index 0000000..301753a
--- /dev/null
+++ b/servo/models/invoices.py
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.db import models
+from django.conf import settings
+from django.utils import timezone
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+
+from django.dispatch import receiver
+from django.db.models.signals import post_save
+
+from django.contrib.sites.managers import CurrentSiteManager
+
+from servo import defaults
+from servo.models import User, Customer, Order, ServiceOrderItem, AbstractOrderItem
+
+
+class Invoice(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ created_at = models.DateTimeField(editable=False, auto_now_add=True)
+ created_by = models.ForeignKey(settings.AUTH_USER_MODEL, editable=False)
+
+ PAYMENT_METHODS = (
+ (0, _("No Charge")),
+ (1, _("Cash")),
+ (2, _("Invoice")),
+ (3, _("Credit Card")),
+ (4, _("Mail payment")),
+ (5, _("Online payment"))
+ )
+
+ payment_method = models.IntegerField(
+ editable=False,
+ choices=PAYMENT_METHODS,
+ default=PAYMENT_METHODS[0][0],
+ verbose_name=_("Payment Method")
+ )
+
+ is_paid = models.BooleanField(default=False, verbose_name=_("paid"))
+ paid_at = models.DateTimeField(null=True, editable=False)
+ order = models.ForeignKey(Order, editable=False)
+ customer = models.ForeignKey(
+ Customer,
+ null=True,
+ editable=False,
+ on_delete=models.SET_NULL
+ )
+
+ # We remember the following the following so that the customer info
+ # on the invoice doesn't change if the customer is modified or deleted
+ customer_name = models.CharField(
+ max_length=255,
+ default=_("Walk-in"),
+ verbose_name=_("Name")
+ )
+ customer_phone = models.CharField(
+ null=True,
+ blank=True,
+ max_length=128,
+ verbose_name=_("Phone")
+ )
+ customer_email = models.CharField(
+ null=True,
+ blank=True,
+ max_length=128,
+ verbose_name=_("Email")
+ )
+ customer_address = models.CharField(
+ null=True,
+ blank=True,
+ max_length=255,
+ verbose_name=_("Address")
+ )
+ reference = models.CharField(
+ null=True,
+ blank=True,
+ max_length=255,
+ verbose_name=_("Reference")
+ )
+
+ total_net = models.DecimalField(max_digits=8, decimal_places=2) # total w/o taxes
+ total_tax = models.DecimalField(max_digits=8, decimal_places=2) # total taxes
+ total_gross = models.DecimalField(max_digits=8, decimal_places=2) # total with taxes
+
+ total_margin = models.DecimalField(
+ max_digits=8,
+ decimal_places=2,
+ editable=False
+ )
+
+ objects = CurrentSiteManager()
+
+ def get_payment_total(self):
+ from django.db.models import Sum
+ result = self.payment_set.all().aggregate(Sum('amount'))
+ return result['amount__sum']
+
+ def get_payment_methods(self):
+ """
+ Returns the different payment methods used in this invoice
+ """
+ payments = self.payment_set.all()
+ return [x.get_method_display() for x in payments]
+
+ def dispatch(self, products):
+ for p in products:
+ soi = ServiceOrderItem.objects.get(pk=p)
+ InvoiceItem.from_soi(soi, self)
+
+ soi.product.sell(soi.amount, self.order.location)
+ soi.dispatched = True
+ soi.save()
+
+ def get_absolute_url(self):
+ from django.core.urlresolvers import reverse
+ return reverse("invoices-view_invoice", args=[self.pk])
+
+ class Meta:
+ ordering = ('-id', )
+ app_label = 'servo'
+ get_latest_by = "id"
+
+
+class InvoiceItem(AbstractOrderItem):
+ invoice = models.ForeignKey(Invoice)
+ price = models.DecimalField(
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Sales Price")
+ )
+
+ @classmethod
+ def from_soi(cls, soi, invoice, invoice_item=None):
+ """
+ Copies SalesOrderItem into an InvoiceItem
+ """
+ if invoice_item:
+ i = invoice_item
+ else:
+ i = cls(invoice=invoice)
+
+ i.sn = soi.sn
+ i.code = soi.code
+ i.title = soi.title
+ i.price = soi.price
+ i.amount = soi.amount
+ i.product = soi.product
+ i.description = soi.description
+ i.created_by = invoice.created_by
+ i.save()
+ return i
+
+ class Meta:
+ app_label = "servo"
+
+
+class Payment(models.Model):
+ invoice = models.ForeignKey(Invoice)
+ METHODS = (
+ (0, _("No Charge")),
+ (1, _("Cash")),
+ (2, _("Invoice")),
+ (3, _("Credit Card")),
+ (4, _("Mail payment")),
+ (5, _("Online payment"))
+ )
+ method = models.IntegerField(
+ choices=METHODS,
+ default=METHODS[0][0],
+ verbose_name=_("Payment Method")
+ )
+ created_by = models.ForeignKey(User)
+ created_at = models.DateTimeField(auto_now_add=True)
+ amount = models.DecimalField(max_digits=8, decimal_places=2)
+
+ class Meta:
+ app_label = "servo"
+
+
+@receiver(post_save, sender=Invoice)
+def trigger_order_dispatched(sender, instance, created, **kwargs):
+ if created:
+ description = _(u'Order %s dispatched') % instance.order.code
+ instance.order.notify('dispatched', description, instance.created_by)
+
+@receiver(post_save, sender=Payment)
+def trigger_payment_received(sender, instance, created, **kwargs):
+ if created:
+ invoice = instance.invoice
+
+ if instance.method > 0:
+ description = _(u'Payment for %0.2f received') % instance.amount
+ invoice.order.notify('paid', description, instance.created_by)
+
+ if invoice.paid_at is None:
+ if invoice.get_payment_total() == invoice.total_gross:
+ invoice.paid_at = timezone.now()
+ invoice.save()
diff --git a/servo/models/note.py b/servo/models/note.py
new file mode 100644
index 0000000..cbdea2f
--- /dev/null
+++ b/servo/models/note.py
@@ -0,0 +1,617 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import base64
+import urllib
+
+from django.db import models, IntegrityError
+
+from django.conf import settings
+from django.utils import timezone
+from django.core.cache import cache
+from django.dispatch import receiver
+from django.utils.html import strip_tags
+from django.core.files.base import ContentFile
+from django.core.exceptions import ValidationError
+
+from django.utils.translation import ugettext_lazy as _
+from django.core.urlresolvers import reverse
+
+from django.core.mail import send_mail, EmailMessage
+
+from django.contrib.sites.models import Site
+
+from django.contrib.contenttypes.fields import GenericRelation
+
+from django.template.defaultfilters import truncatechars
+from django.db.models.signals import pre_delete, post_save
+
+from mptt.managers import TreeManager
+from django.contrib.sites.managers import CurrentSiteManager
+
+from mptt.models import MPTTModel, TreeForeignKey
+
+from servo import defaults
+from servo.lib.shorturl import from_time
+
+from servo.models.order import Order
+from servo.models.account import User
+from servo.models.customer import Customer
+from servo.models.escalations import Escalation
+from servo.models.common import Configuration, Tag, Attachment, Event
+
+
+SMS_ENCODING = 'ISO-8859-15'
+COOKIE_REGEX = r'\(SRO#([\w/]+)\).*$'
+
+
+class UnsavedForeignKey(models.ForeignKey):
+ # A ForeignKey which can point to an unsaved object
+ allow_unsaved_instance_assignment = True
+
+
+def clean_phone_number(number):
+ return re.sub(r'[\+\s\-]', '', number).strip()
+
+
+def validate_phone_number(number):
+ match = re.match(r'([\+\d]+$)', number)
+ if match:
+ return match.group(1).strip()
+ else:
+ raise ValidationError(_(u'%s is not a valid phone number') % number)
+
+
+class Note(MPTTModel):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ subject = models.CharField(
+ blank=True,
+ max_length=255,
+ default=defaults.subject,
+ verbose_name=_('Subject'),
+ )
+
+ body = models.TextField(verbose_name=_('Message'))
+
+ code = models.CharField(
+ unique=True,
+ max_length=9,
+ editable=False,
+ default=from_time
+ )
+ sender = models.CharField(
+ default='',
+ max_length=255,
+ verbose_name=_('From')
+ )
+ recipient = models.CharField(
+ blank=True,
+ default='',
+ max_length=255,
+ verbose_name=_('To')
+ )
+ customer = models.ForeignKey(Customer, null=True, blank=True)
+ escalation = UnsavedForeignKey(Escalation, null=True, editable=False)
+ labels = models.ManyToManyField(Tag, blank=True, limit_choices_to={'type': 'note'})
+
+ events = GenericRelation(Event)
+ attachments = GenericRelation(Attachment, null=True, blank=True)
+ parent = TreeForeignKey(
+ 'self',
+ null=True,
+ blank=True,
+ related_name='replies'
+ )
+
+ created_at = models.DateTimeField(auto_now_add=True, editable=False)
+ created_by = models.ForeignKey(settings.AUTH_USER_MODEL, editable=False)
+ sent_at = models.DateTimeField(null=True, editable=False)
+ order = models.ForeignKey(Order, null=True, blank=True)
+
+ is_reported = models.BooleanField(default=False, verbose_name=_("report"))
+ is_read = models.BooleanField(
+ default=True,
+ editable=False,
+ verbose_name=_("read")
+ )
+ is_flagged = models.BooleanField(
+ default=False,
+ editable=False,
+ verbose_name=_("flagged")
+ )
+
+ objects = TreeManager()
+ on_site = CurrentSiteManager()
+
+ def __render__(self, tpl, ctx):
+ from django import template
+ tpl = template.Template(tpl)
+ return tpl.render(template.Context(ctx))
+
+ def render_subject(self, ctx):
+ """
+ Renders this Markdown body
+ """
+ self.subject = self.__render__(self.subject, ctx)
+ return self.subject
+
+ def render_body(self, ctx):
+ """
+ Renders this Markdown body
+ """
+ self.body = self.__render__(self.body, ctx)
+ return self.body
+
+ def add_reply(self, note):
+ note.parent = self
+ note.order = self.order
+ note.escalation = self.escalation
+
+ def zip_attachments(self):
+ pass
+
+ def get_default_sender(self):
+ return Configuration.get_default_sender(self.created_by)
+
+ def get_sender_choices(self):
+ """
+ Returns the options for this note's senders
+ """
+ choices = []
+ addresses = []
+ user = self.created_by
+ loc = user.location
+ def_email = self.get_default_sender()
+
+ if user.email:
+ user_choice = (user.email, u'%s <%s>' % (user.get_name(), user.email),)
+ choices.append(user_choice)
+ addresses.append(user.email)
+
+ if loc.email and loc.email not in addresses:
+ loc_choice = (loc.email, u'%s <%s>' % (loc.title, loc.email),)
+ choices.append(loc_choice)
+ addresses.append(loc.email)
+
+ if def_email and def_email not in addresses:
+ def_choice = (def_email, _(u'Default Address <%s>') % def_email,)
+ choices.append(def_choice)
+
+ return choices
+
+ def quote(self):
+ return "> " + self.body
+
+ def unquote(self):
+ return re.sub(r'^>.*', '', self.body, flags=re.MULTILINE).strip()
+
+ def clean_subject(self):
+ return re.sub(COOKIE_REGEX, '', self.subject)
+
+ def get_excluded_emails(self):
+ """
+ Returns a list of email addresses that should not be contacted
+ """
+ if not cache.get('nomail'):
+ User.refresh_nomail()
+
+ return cache.get('nomail')
+
+ def get_classes(self):
+ """
+ Returns the appropriate CSS classes for this note
+ """
+ classes = list()
+
+ if not self.is_read:
+ classes.append('info')
+
+ if self.is_reported:
+ classes.append('success')
+
+ if self.is_flagged:
+ classes.append('warning')
+
+ return ' '.join(classes)
+
+ def find_parent(self, txt):
+ cookie = re.search(r'\(SRO#([\w/]+)\)', txt)
+
+ if not cookie:
+ return
+
+ parent_code, order_code = cookie.group(1).split('/')
+
+ try:
+ parent = Note.objects.get(code=parent_code)
+ self.parent = parent
+ self.recipient = parent.sender
+ self.order_id = parent.order_id
+ except Note.DoesNotExist:
+ # original note has been deleted
+ self.order = Order.objects.get(url_code=order_code)
+
+ @classmethod
+ def from_email(cls, msg, user):
+ """
+ Creates a new Note from an email message
+ """
+ note = cls(sender=msg['From'], created_by=user)
+
+ note.is_read = False
+ note.is_reported = False
+ note.recipient = msg['To']
+ note.subject = msg['Subject']
+
+ note.find_parent(note.subject)
+
+ for part in msg.walk():
+ t, s = part.get_content_type().split('/', 1)
+ charset = part.get_content_charset() or "latin1"
+
+ if t == "text":
+ payload = part.get_payload(decode=True)
+ note.body = unicode(payload, str(charset), "ignore")
+ if s == "html":
+ note.body = strip_tags(note.body)
+ else:
+ note.save()
+ if part.get_filename():
+ filename = unicode(part.get_filename())
+ content = base64.b64decode(part.get_payload())
+ content = ContentFile(content, filename)
+ attachment = Attachment(content=content, content_object=note)
+ attachment.save()
+ attachment.content.save(filename, content)
+ note.attachments.add(attachment)
+
+ if not note.parent:
+ # cookie not found in the subject, let's try the body...
+ note.find_parent(note.body)
+
+ note.save()
+
+ return note
+
+ def get_sender_name(self):
+ name = self.created_by.get_full_name()
+ if not name:
+ name = self.created_by.username
+
+ return name
+
+ def get_flags(self):
+ return ['unread', 'flagged', 'reported']
+
+ def get_reported_title(self):
+ return _("As Unreported") if self.is_reported else _("As Reported")
+
+ def get_read_title(self):
+ return _("As Unread") if self.is_read else _("As Read")
+
+ def get_flagged_title(self):
+ return _("As Unflagged") if self.is_flagged else _("As Flagged")
+
+ def mailto(self):
+ """
+ Returns the email recipients of this note
+ Don't use validate_email because addresses may also be in
+ Name <email> format (replies to emails)
+ """
+ to = []
+ recipients = [r.strip() for r in self.recipient.split(',')]
+ for r in recipients:
+ m = re.search(r'([\w\.\-_]+@[\w\.\-_]+)', r, re.IGNORECASE)
+ if m:
+ to.append(m.group(0))
+
+ return ','.join(to)
+
+ def get_indent(self):
+ return (self.level*20)+10
+
+ def notify(self, action, message, user):
+ e = Event(content_object=self, action=action)
+ e.description = message
+ e.triggered_by = user
+ e.save()
+
+ def get_edit_url(self):
+ if self.order:
+ return reverse('orders-edit_note', args=[self.order.pk, self.pk])
+
+ def has_sent_message(self, recipient):
+ r = self.message_set.filter(recipient=recipient)
+ return r.exclude(status='FAILED').exists()
+
+ def send_mail(self, user):
+ """
+ Sends this note as an email
+ """
+ mailto = self.mailto()
+
+ # Only send the same note once
+ if self.has_sent_message(mailto):
+ raise ValueError(_('Already sent message to %s') % mailto)
+
+ config = Configuration.conf()
+ smtp_host = config.get('smtp_host').split(':')
+ settings.EMAIL_HOST = smtp_host[0]
+
+ if len(smtp_host) > 1:
+ settings.EMAIL_PORT = int(smtp_host[1])
+
+ settings.EMAIL_USE_TLS = config.get('smtp_ssl')
+ settings.EMAIL_HOST_USER = str(config.get('smtp_user'))
+ settings.EMAIL_HOST_PASSWORD = str(config.get('smtp_password'))
+
+ headers = {}
+ headers['Reply-To'] = self.sender
+ headers['References'] = '%s.%s' % (self.code, self.sender)
+ subject = u'%s (SRO#%s)' % (self.subject, self.code)
+
+ if self.order:
+ # Encode the SO code so that we can match replies to the SO
+ # even if the original note has been deleted
+ subject = u'%s (SRO#%s/%s)' % (self.subject,
+ self.code,
+ self.order.url_code)
+
+ recipients = mailto.split(',')
+
+ msg = EmailMessage(subject,
+ self.body,
+ self.sender,
+ recipients,
+ headers=headers)
+
+ for f in self.attachments.all():
+ msg.attach_file(f.content.path)
+
+ msg.send()
+
+ for r in recipients:
+ msg = Message(note=self, recipient=r, created_by=user, body=self.body)
+ msg.sent_at = timezone.now()
+ msg.sender = self.sender
+ msg.status = 'SENT'
+ msg.save()
+
+ message = _(u'Message sent to %s') % mailto
+ self.notify('email_sent', message, user)
+ return message
+
+ def send_sms_smtp(self, config, recipient):
+ """
+ Sends SMS through SMTP gateway
+ """
+ recipient = recipient.replace(' ', '')
+ settings.EMAIL_HOST = config.get('smtp_host')
+ settings.EMAIL_USE_TLS = config.get('smtp_ssl')
+ settings.EMAIL_HOST_USER = config.get('smtp_user')
+ settings.EMAIL_HOST_PASSWORD = config.get('smtp_password')
+
+ send_mail(recipient, self.body, self.sender, [config['sms_smtp_address']])
+
+ def send_sms_builtin(self, recipient, sender=None):
+ """
+ Sends SMS through built-in gateway
+ """
+ if not settings.SMS_HTTP_URL:
+ raise ValueError(_('System is not configured for built-in SMS support.'))
+
+ if sender is None:
+ location = self.created_by.location
+ sender = location.title
+
+ data = urllib.urlencode({
+ 'username': settings.SMS_HTTP_USERNAME,
+ 'password': settings.SMS_HTTP_PASSWORD,
+ 'numberto': recipient.replace(' ', ''),
+ 'numberfrom': sender.encode(SMS_ENCODING),
+ 'message': self.body.encode(SMS_ENCODING),
+ })
+
+ from ssl import _create_unverified_context
+ f = urllib.urlopen(settings.SMS_HTTP_URL, data, context=_create_unverified_context())
+ return f.read()
+
+ def send_sms(self, number, user):
+ """
+ Sends message as SMS
+ """
+ number = validate_phone_number(number)
+
+ if self.has_sent_message(number):
+ raise ValueError(_('Already sent message to %s') % number)
+
+ conf = Configuration.conf()
+ sms_gw = conf.get('sms_gateway')
+
+ if not sms_gw:
+ raise ValueError(_("SMS gateway not configured"))
+
+ msg = Message(note=self, recipient=number, created_by=user, body=self.body)
+
+ if sms_gw == 'hqsms':
+ from servo.messaging.sms import HQSMSProvider
+ HQSMSProvider(number, self, msg).send()
+
+ if sms_gw == 'jazz':
+ from servo.messaging.sms import SMSJazzProvider
+ SMSJazzProvider(number, self, msg).send()
+ #self.send_sms_jazz(number, conf.get('sms_http_sender', ''), msg)
+
+ if sms_gw == 'http':
+ from servo.messaging.sms import HttpProvider
+ HttpProvider(self, number).send()
+
+ if sms_gw == 'smtp':
+ gw_address = conf.get('sms_smtp_address')
+
+ if not gw_address:
+ raise ValueError('Missing SMTP SMS gateway address')
+
+ self.send_sms_smtp(conf, number)
+
+ if sms_gw == 'builtin':
+ self.send_sms_builtin(number)
+
+ msg.method = 'SMS'
+ msg.status = 'SENT'
+ msg.sent_at = timezone.now()
+ msg.save()
+
+ message = _('Message sent to %s') % number
+ self.notify('sms_sent', message, self.created_by)
+ return message
+
+ def send_and_save(self, user):
+ """
+ The main entry point to the sending logic
+ """
+ from django.utils.encoding import force_text
+ messages = list()
+ recipients = [r.strip() for r in self.recipient.split(',')]
+
+ for r in recipients:
+ try:
+ messages.append(self.send_sms(r, user))
+ except (ValidationError, IntegrityError), e:
+ pass
+
+ if self.mailto():
+ messages.append(self.send_mail(user))
+
+ esc = self.escalation
+
+ if esc and esc.pk and esc.issue_type:
+ if esc.submitted_at is None:
+ esc.submit()
+ messages.append(_('Escalation %s created') % esc.escalation_id)
+ else:
+ esc.update(self.body)
+ messages.append(_('Escalation %s updated') % esc.escalation_id)
+
+ self.save()
+
+ if len(messages) < 1:
+ messages = [_('Note saved')]
+
+ return ', '.join([force_text(m) for m in messages])
+
+ def get_absolute_url(self):
+ if self.order:
+ return "%s#note-%d" % (self.order.get_absolute_url(), self.pk)
+ else:
+ return "/notes/saved/%d/view/" % self.pk
+
+ def __unicode__(self):
+ return str(self.pk)
+
+ class Meta:
+ app_label = "servo"
+ get_latest_by = "created_at"
+
+
+class Message(models.Model):
+ """
+ A note being sent by some method (SMS, email, escalation).
+ Only one sender and recipient per message
+ Keeping this separate from Note so that we can send and track
+ messages separately from Notes
+ """
+ note = models.ForeignKey(Note)
+ code = models.CharField(unique=True, max_length=36, default=defaults.uid)
+ created_by = models.ForeignKey(User)
+ sender = models.CharField(max_length=128)
+ recipient = models.CharField(max_length=128)
+ body = models.TextField()
+ sent_at = models.DateTimeField(null=True)
+ received_at = models.DateTimeField(null=True)
+ STATUSES = (
+ ('SENT', 'SENT'),
+ ('DELIVERED', 'DELIVERED'),
+ ('RECEIVED', 'RECEIVED'),
+ ('FAILED', 'FAILED'),
+ )
+ status = models.CharField(max_length=16, choices=STATUSES)
+ METHODS = (
+ ('EMAIL', 'EMAIL'),
+ ('SMS', 'SMS'),
+ ('GSX', 'GSX'),
+ )
+ method = models.CharField(
+ max_length=16,
+ choices=METHODS,
+ default=METHODS[0][0]
+ )
+ error = models.TextField()
+
+ def send(self):
+ result = None
+ self.recipient = self.recipient.strip()
+
+ try:
+ validate_phone_number(self.recipient)
+ result = self.send_sms()
+ except ValidationError:
+ pass
+
+ try:
+ validate_email(self.recipient)
+ result = self.send_mail()
+ except ValidationError:
+ pass
+
+ self.save()
+ return result
+
+ class Meta:
+ app_label = "servo"
+ unique_together = ('note', 'recipient')
+
+
+@receiver(pre_delete, sender=Note)
+def clean_files(sender, instance, **kwargs):
+ instance.attachments.all().delete()
+
+
+@receiver(post_save, sender=Note)
+def note_saved(sender, instance, created, **kwargs):
+ if created and instance.order:
+ order = instance.order
+ user = instance.created_by
+
+ if user is not order.user:
+ msg = truncatechars(instance.body, 75)
+ order.notify("note_added", msg, user)
+
diff --git a/servo/models/order.py b/servo/models/order.py
new file mode 100644
index 0000000..d4f1fe3
--- /dev/null
+++ b/servo/models/order.py
@@ -0,0 +1,1156 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from datetime import timedelta
+from django.db import models, IntegrityError
+
+from django.conf import settings
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+from django.contrib.sites.managers import CurrentSiteManager
+
+from django.contrib.contenttypes.fields import GenericRelation
+
+from django.dispatch import receiver
+from django.core.urlresolvers import reverse
+from django.db.models.signals import pre_save, post_save, post_delete
+
+from servo import defaults
+from servo.lib.shorturl import encode_url
+
+from servo.models.common import Tag, Location, Event, Configuration, GsxAccount
+from servo.models.product import *
+from servo.models.customer import Customer
+from servo.models.device import Device
+from servo.models.queue import Queue, Status, QueueStatus
+
+
+class Order(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ code = models.CharField(max_length=8, unique=True, null=True)
+ url_code = models.CharField(max_length=8, unique=True, null=True)
+ # Device description or something else
+ description = models.CharField(max_length=128, default="")
+ status_icon = models.CharField(max_length=16, default="undefined")
+
+ priority = models.IntegerField(
+ default=Queue.PRIO_NORMAL,
+ choices=Queue.PRIORITIES,
+ verbose_name=_("priority")
+ )
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ created_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ related_name="created_orders",
+ on_delete=models.SET_NULL
+ )
+
+ started_at = models.DateTimeField(null=True)
+ started_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ related_name="started_orders",
+ on_delete=models.SET_NULL
+ )
+
+ closed_at = models.DateTimeField(null=True)
+ closed_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ related_name='closed_orders',
+ on_delete=models.SET_NULL
+ )
+ followed_by = models.ManyToManyField(
+ settings.AUTH_USER_MODEL,
+ related_name="followed_orders"
+ )
+
+ tags = models.ManyToManyField(Tag, verbose_name="tags")
+ events = GenericRelation(Event)
+
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ on_delete=models.PROTECT
+ )
+
+ checkin_location = models.ForeignKey(
+ Location,
+ null=True,
+ related_name='+',
+ on_delete=models.SET_NULL
+ )
+
+ location = models.ForeignKey(Location, on_delete=models.PROTECT)
+ checkout_location = models.ForeignKey(
+ Location,
+ null=True,
+ related_name='+',
+ on_delete=models.SET_NULL
+ )
+
+ place = models.CharField(default='', max_length=128)
+ customer = models.ForeignKey(
+ Customer,
+ null=True,
+ related_name='orders',
+ on_delete=models.SET_NULL
+ )
+ customer_name = models.CharField(max_length=128, default='')
+
+ devices = models.ManyToManyField(Device, through="OrderDevice")
+ products = models.ManyToManyField(Product, through="ServiceOrderItem")
+
+ queue = models.ForeignKey(
+ Queue,
+ null=True,
+ verbose_name=_("queue"),
+ on_delete=models.SET_NULL
+ )
+ status = models.ForeignKey(
+ QueueStatus,
+ null=True,
+ verbose_name=_("status"),
+ on_delete=models.SET_NULL
+ )
+
+ statuses = models.ManyToManyField(
+ Status,
+ through="OrderStatus",
+ related_name="orders"
+ )
+
+ STATE_QUEUED = 0 # order hasn't been started
+ STATE_OPEN = 1 # order is being worked on
+ STATE_CLOSED = 2 # order is closed
+
+ STATES = (
+ (STATE_QUEUED, _("Unassigned")),
+ (STATE_OPEN, _("Open")),
+ (STATE_CLOSED, _("Closed"))
+ )
+
+ state = models.IntegerField(default=0, choices=STATES)
+
+ status_name = models.CharField(max_length=128, default="")
+ status_started_at = models.DateTimeField(null=True)
+ status_limit_green = models.DateTimeField(null=True) # turn yellow after this
+ status_limit_yellow = models.DateTimeField(null=True) # turn red after this
+
+ objects = CurrentSiteManager()
+ api_fields = ('status_name', 'status_description',)
+
+ def apply_rules(self):
+ pass
+
+ def get_accessories(self):
+ return Accessory.objects.filter(order=self)
+
+ def set_accessories(self, accs, device):
+ for a in accs:
+ a = Accessory(name=a, order=self, device=device)
+ a.save()
+
+ def add_tag(self, tag, user):
+ from servo.tasks import apply_rules
+
+ if not isinstance(tag, Tag):
+ tag = Tag.objects.get(pk=tag)
+
+ self.tags.add(tag)
+
+ event = Event(content_object=self)
+ event.description = str(tag.pk)
+ event.action = "set_tag"
+ event.triggered_by = user
+
+ apply_rules(event)
+
+ def set_tags(self, tags, user):
+ return [self.add_tag(t, user) for t in tags]
+
+ def check_in(self, user):
+ """
+ Checks this Order in through the check-in process
+ """
+ queue_id = Configuration.conf('checkin_queue')
+ self.set_queue(queue_id, user)
+
+ def can_order_products(self):
+ return self.products.count() > 0 and self.is_editable
+
+ def duplicate(self, user):
+ new_order = Order(customer=self.customer, created_by=user)
+ new_order.save()
+ new_order.set_queue(self.queue_id, user)
+
+ for d in self.devices.all():
+ new_order.add_device(d, user)
+
+ return new_order
+
+ def get_print_template(self, kind="confirmation"):
+ template = "orders/print_%s.html" % kind
+
+ if self.queue:
+ queue = self.queue
+
+ if kind == "confirmation" and queue.order_template:
+ template = queue.order_template.name
+ if kind == "quote" and queue.quote_template:
+ template = queue.quote_template.name
+ if kind == "receipt" and queue.receipt_template:
+ template = queue.receipt_template.name
+ if kind == "dispatch" and queue.dispatch_template:
+ template = queue.dispatch_template.name
+
+ return template
+
+ def get_repairs(self):
+ # Returns the active GSX repairs for this SO
+ return self.repair_set.exclude(submitted_at=None)
+
+ def get_repair(self):
+ # Returns the latest GSX repair for this SO
+ try:
+ return self.get_repairs().latest()
+ except Exception:
+ return
+
+ def get_similar(self, status, state):
+ # Returns a queryset of "similar" cases
+ if self.user is None:
+ return Order.objects.filter(status=status)
+
+ return Order.objects.filter(user=self.user)
+
+ def get_footer(self):
+ footer = self.code
+ repair = self.get_repair()
+ if repair:
+ footer += ' (%s)' % repair.confirmation
+ return footer
+
+ def add_device(self, device, user):
+ try:
+ OrderDevice.objects.create(order=self, device=device)
+ event = _(u'%s added') % device.description
+ self.notify('device_added', event, user)
+ return event
+ except IntegrityError:
+ raise ValueError(_("This device has already been added to this order"))
+
+ def add_device_sn(self, sn, user):
+ """
+ Adds device to order using serial number
+ """
+ sn = sn.upper()
+ try:
+ device = Device.objects.get(sn=sn)
+ except Device.DoesNotExist:
+ device = Device.from_gsx(sn)
+ device.save()
+
+ self.add_device(device, user)
+ return device
+
+ def remove_device(self, device, user):
+ OrderDevice.objects.filter(order=self, device=device).delete()
+ msg = _(u'%s removed') % device.description
+ self.notify('device_removed', msg, user)
+ return msg
+
+ def get_available_users(self, user):
+ """
+ Returns a list of users available to work on this order
+ """
+ if self.queue:
+ return self.queue.user_set.filter(is_active=True)
+
+ return user.location.user_set.filter(is_active=True)
+
+ def get_title(self):
+ """
+ Returns a human-readable title for this order, based on various criteria
+ """
+ from django.utils.timesince import timesince
+ now = timezone.now()
+ moment_seconds = 120
+
+ if self.closed_at:
+ if (now - self.closed_at).seconds < moment_seconds:
+ return _("Closed a moment ago")
+ return _(u"Closed for %(time)s") % {'time': timesince(self.closed_at)}
+
+ if self.status and self.status_started_at is not None:
+ if (now - self.status_started_at).seconds < moment_seconds:
+ return _(u"%s a moment ago") % self.status.status.title
+ delta = timesince(self.status_started_at)
+ d = {'status': self.status.status.title, 'time': delta}
+ return _("%(status)s for %(time)s" % d)
+
+ if self.user is None:
+ if (now - self.created_at).seconds < moment_seconds:
+ return _("Created a moment ago")
+ return _("Unassigned for %(delta)s") % {'delta': timesince(self.created_at)}
+
+ if self.started_at and self.user is not None:
+ if (now - self.started_at).seconds < moment_seconds:
+ return _("Started a moment ago")
+ return _("Open for %(delta)s") % {'delta': timesince(self.started_at)}
+
+ def get_place(self):
+ return self.place or _("Select place")
+
+ def get_status(self):
+ return self.status or _("Select status")
+
+ def get_user_name(self):
+ if self.user is not None:
+ return self.user.get_full_name()
+
+ def get_user(self):
+ return self.user or _("Select user")
+
+ def get_queue(self):
+ return self.queue or _("Select queue")
+
+ def get_queue_url(self):
+ return reverse("orders-index")
+
+ def get_queue_title(self):
+ if self.queue:
+ return self.queue.title
+ else:
+ return _("Orders")
+
+ def is_item_complete(self, item):
+ try:
+ return self.checklistitemvalue_set.get(item=item)
+ except Exception:
+ return False
+
+ def close(self, user):
+ self.notify("close_order", _(u"Order %s closed") % self.code, user)
+ self.closed_by = user
+ self.closed_at = timezone.now()
+ self.state = self.STATE_CLOSED
+ self.save()
+
+ if Configuration.autocomplete_repairs():
+ for r in self.repair_set.filter(completed_at=None):
+ r.close(user)
+
+ if self.queue and self.queue.status_closed:
+ self.set_status(self.queue.status_closed, user)
+
+ def reopen(self, user):
+ self.state = Order.STATE_OPEN
+ self.closed_at = None
+ self.save()
+ msg = _("Order %s reopened") % self.code
+ self.notify("reopen", msg, user)
+ return msg
+
+ def notes(self):
+ return self.note_set.all()
+
+ def reported_notes(self):
+ return self.note_set.filter(is_reported=True)
+
+ @property
+ def can_create_carryin(self):
+ return self.customer and self.queue and self.is_editable
+
+ def get_status_name(self):
+ try:
+ return self.status.status.title
+ except Exception:
+ pass
+
+ def get_status_description(self):
+ try:
+ return self.status.status.description
+ except Exception:
+ pass
+
+ def get_status_id(self):
+ """
+ Returns "real" status ID of this order (regardless of queue)
+ """
+ if self.status:
+ return self.status.status.id
+
+ def get_next(self):
+ try:
+ result = Order.objects.filter(pk__gt=self.pk, queue=self.queue)
+ return result[0].pk
+ except Exception:
+ pass
+
+ def get_prev(self):
+ try:
+ result = Order.objects.filter(pk__lt=self.pk, queue=self.queue)
+ return result[0].pk
+ except Exception:
+ pass
+
+ def get_color(self):
+ color = "undefined"
+ if self.status:
+ now = timezone.now()
+ if now > self.status_limit_yellow:
+ color = "danger"
+ if now < self.status_limit_yellow:
+ color = "warning"
+ if now < self.status_limit_green:
+ color = "success"
+
+ return color
+
+ def get_status_img(self):
+ color = self.get_color()
+ return "images/status_%s_16.png" % color
+
+ def set_property(self, key, value):
+ pass
+
+ def set_location(self, new_location, user):
+ self.location = new_location
+ msg = _(u"Order %s moved to %s") % (self.code, new_location.title)
+ self.notify("set_location", msg, user)
+ self.save()
+ return msg
+
+ def set_checkin_location(self, new_location, user):
+ pass
+
+ def notify(self, action, message, user):
+ """
+ Notifies this order of an event
+ This is also the hub for automation handling
+ """
+ if self.is_closed:
+ return
+
+ e = Event(content_object=self, action=action)
+ e.description = message
+ e.triggered_by = user
+ e.save()
+
+ for f in self.followed_by.exclude(pk=user.pk).exclude(should_notify=False):
+ e.notify_users.add(f)
+
+ if action == "product_arrived":
+ if self.queue and self.queue.status_products_received:
+ new_status = self.queue.status_products_received
+ self.set_status(new_status, user)
+
+ def set_status(self, new_status, user):
+ """
+ Sets status of this order to new_status
+ Status can only be set if order belongs to a queue!
+ """
+ if self.is_closed:
+ return # fail silently
+
+ if self.queue is None:
+ raise ValueError(_('Order must belong to a queue to set status'))
+
+ if isinstance(new_status, QueueStatus):
+ status = new_status
+ else:
+ if int(new_status) == 0:
+ return self.unset_status(user)
+
+ status = QueueStatus.objects.get(pk=new_status)
+
+ self.status = status
+ self.status_name = status.status.title
+ self.status_started_at = timezone.now()
+
+ self.status_limit_green = status.get_green_limit()
+ self.status_limit_yellow = status.get_yellow_limit()
+ self.save()
+
+ # Set up the OrderStatus
+ OrderStatus.create(self, status, user)
+
+ # trigger the notification
+ self.notify("set_status", self.status_name, user)
+
+ def unset_status(self, user):
+ if self.is_closed:
+ return # fail silently
+
+ self.status = None
+ self.status_started_at = None
+ self.status_limit_green = None
+ self.status_limit_yellow = None
+ self.save()
+
+ self.notify("set_status", _('Status unassigned'), user)
+
+ def set_queue(self, queue_id, user):
+ if self.is_closed:
+ return
+
+ if queue_id in (None, ''):
+ queue_id = 0
+
+ if isinstance(queue_id, Queue):
+ queue = queue_id
+ else:
+ if int(queue_id) == 0:
+ return self.unset_queue(user)
+
+ queue = Queue.objects.get(pk=queue_id)
+
+ self.queue = queue
+ self.priority = queue.priority
+ self.notify('set_queue', queue.title, user)
+
+ if queue.gsx_soldto:
+ gsx_account = GsxAccount.get_account(self.location, queue)
+ for i in self.repair_set.filter(completed_at=None):
+ i.gsx_account = gsx_account
+ i.save()
+
+ if queue.status_created:
+ self.set_status(queue.status_created, user)
+ else:
+ self.save()
+
+ def unset_queue(self, user):
+ self.queue = None
+ self.notify('set_queue', _('Removed from queue'), user)
+ self.save()
+
+ def add_follower(self, follower):
+ if follower in self.followed_by.all():
+ return
+
+ self.followed_by.add(follower)
+
+ def remove_follower(self, follower):
+ if self.state == self.STATE_CLOSED:
+ raise ValueError(_('Closed orders cannot be modified'))
+
+ self.followed_by.remove(follower)
+
+ def toggle_follower(self, follower):
+ if follower in order.followed_by.all():
+ self.remove_follower(user)
+ else:
+ self.add_follower(follower)
+
+ def set_user(self, new_user, current_user):
+ """
+ Sets the assignee of this order to new_user
+ """
+ if self.state == self.STATE_CLOSED:
+ raise ValueError(_('Closed orders cannot be modified'))
+
+ state = self.STATE_OPEN
+
+ if new_user is None:
+ state = self.STATE_QUEUED
+ event = _("Order unassigned")
+ self.remove_follower(self.user)
+ else:
+ data = {'order': self.code, 'user': new_user.get_full_name()}
+ event = _(u"Order %(order)s assigned to %(user)s") % data
+ # The assignee should also be a follower
+ self.add_follower(new_user)
+
+ self.user = new_user
+ self.state = state
+
+ self.notify("set_user", event, current_user)
+
+ if self.user is not None:
+ self.location = new_user.location
+ if self.started_by is None:
+ self.started_by = new_user
+ self.started_at = timezone.now()
+ queue = self.queue
+ if queue and queue.status_assigned:
+ self.set_status(queue.status_assigned, current_user)
+
+ self.save()
+
+ def customer_id(self):
+ return self.customer.id
+
+ def customer_list(self):
+ """
+ Returns this order's customer wrapped in a list for easier
+ tree recursion
+ """
+ return [self.customer]
+
+ def customer_tree(self):
+ if self.customer is None:
+ return '0'
+ else:
+ return self.customer.tree_id
+
+ def has_devices(self):
+ return self.devices.all().count() > 0
+
+ def device_name(self):
+ if self.devices.count():
+ return self.devices.all()[0].description
+
+ def set_customer(self, new_customer):
+ self.customer = new_customer
+ self.save()
+
+ def get_customer_name(self):
+ try:
+ return self.customer.fullname
+ except AttributeError:
+ pass
+
+ def device_slug(self):
+ try:
+ return self.devices.all()[0].slug
+ except Exception:
+ return None
+
+ def net_total(self):
+ total = 0
+
+ for p in self.serviceorderitem_set.filter(should_report=True):
+ total += p.price_notax() * p.amount
+
+ return total
+
+ def gross_total(self):
+ total = 0
+
+ for p in self.serviceorderitem_set.filter(should_report=True):
+ total += p.price * p.amount
+
+ return total
+
+ def total_tax(self):
+ return self.gross_total() - self.net_total()
+
+ def add_product(self, product, amount, user):
+ """
+ Adds this product to the Service Order with stock price
+ """
+ oi = ServiceOrderItem(order=self, created_by=user)
+ oi.product = product
+ oi.code = product.code
+ oi.title = product.title
+ oi.description = product.description
+ oi.amount = amount
+
+ oi.price_category = 'stock'
+ oi.price = product.price_sales_stock
+
+ oi.save()
+
+ self.notify("product_added", _('Product %s added') % oi.title, user)
+
+ return oi
+
+ def remove_product(self, oi, user):
+ oi.delete()
+ msg = _('Product %s removed from order') % oi.title
+ self.notify("product_removed", msg, user)
+ return msg
+
+ @property
+ def can_dispatch(self):
+ undispatched = self.products.filter(dispatched=False)
+ return undispatched.count() > 0 and self.is_editable
+
+ @property
+ def can_close(self):
+ return self.is_editable and not self.can_dispatch
+
+ def dispatch(self, invoice, products):
+ """
+ Dispatch these products from the inventory with this invoice
+ """
+ invoice.dispatch(products)
+ if self.queue and self.queue.status_dispatched:
+ self.set_status(self.queue.status_dispatched, invoice.created_by)
+
+ def total_margin(self):
+ """
+ Calculates the total margin for this Service Order
+ """
+ total_purchase_price = 0
+ for p in self.serviceorderitem_set.filter(should_report=True):
+ total_purchase_price += p.get_product_price('purchase') * p.amount
+
+ return (self.net_total() - Decimal(total_purchase_price))
+
+ @property
+ def products(self):
+ return self.serviceorderitem_set.filter(should_report=True)
+
+ @property
+ def serialized_products(self):
+ return self.products.filter(product__is_serialized=True)
+
+ def get_parts(self):
+ """
+ Returns the GSX parts that can be ordered for this SRO
+ """
+ return [x for x in self.products.all() if x.product.is_apple_part]
+
+ @property
+ def has_parts(self):
+ return len(self.get_parts()) > 0
+
+ def get_devices(self):
+ return self.devices.filter(orderdevice__should_report=True)
+
+ def get_first_device(self):
+ try:
+ return self.get_devices().first()
+ except Exception:
+ pass
+
+ @property
+ def is_editable(self):
+ return self.closed_at is None
+
+ @property
+ def is_closed(self):
+ return self.closed_at is not None
+
+ @property
+ def has_products(self):
+ return self.products.count() > 0
+
+ def has_accessories(self):
+ return self.accessory_set.all().count() > 0
+
+ def get_accessories(self):
+ return self.accessory_set.values_list('name', flat=True)
+
+ class Meta:
+ app_label = 'servo'
+ ordering = ('-priority', 'id',)
+
+ permissions = (
+ ("change_user", _("Can set assignee")),
+ ("change_status", _("Can change status")),
+ ("follow_order", _("Can follow order")),
+ ("copy_order", _("Can copy order")),
+ ("batch_process", _("Can batch process")),
+ )
+
+ def get_absolute_url(self):
+ return reverse("orders-edit", args=[self.pk])
+
+ def __unicode__(self):
+ return self.code
+
+
+class AbstractOrderItem(models.Model):
+ """
+ The base class for order lines (purchase, sales)
+ """
+ product = models.ForeignKey(Product, on_delete=models.PROTECT)
+ code = models.CharField(blank=True, default='', max_length=128)
+ title = models.CharField(max_length=128, verbose_name=_("title"))
+ description = models.TextField(
+ blank=True,
+ default='',
+ verbose_name=_("description")
+ )
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ created_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ editable=False,
+ on_delete=models.SET_NULL
+ )
+
+ amount = models.IntegerField(default=1, verbose_name=_("amount"))
+ sn = models.CharField(
+ blank=True,
+ default="",
+ max_length=32,
+ verbose_name=_("KGB Serial Number")
+ )
+
+ def price_notax(self):
+ """
+ Returns the price of this OI w/o VAT
+ """
+ from decimal import ROUND_05UP
+ vat_pct = self.product.pct_vat
+ return (self.price/Decimal((100+vat_pct)/100)).quantize(Decimal('1.00'))
+
+ def total_gross(self):
+ return self.price * self.amount
+
+ def total_net(self):
+ return self.price_notax() * self.amount
+
+ def total_tax(self):
+ """
+ Returns the amount of VAT paid for this POI
+ """
+ return (self.price - self.price_notax()) * self.amount
+
+ class Meta:
+ abstract = True
+
+
+class ServiceOrderItem(AbstractOrderItem):
+ """
+ A product that has been added to a Service Order
+ """
+ order = models.ForeignKey(Order)
+
+ dispatched = models.BooleanField(
+ default=False,
+ verbose_name=_("dispatched")
+ )
+ should_report = models.BooleanField(
+ default=True,
+ verbose_name=_("report")
+ )
+ price = models.DecimalField(
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_('sales price')
+ )
+
+ replaced_at = models.DateTimeField(null=True)
+ replaced_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ editable=False,
+ related_name='replaced_parts'
+ )
+
+ kbb_sn = models.CharField(
+ blank=True,
+ default="",
+ max_length=32,
+ verbose_name=_("KBB Serial Number")
+ )
+
+ imei = models.CharField(
+ blank=True,
+ default="",
+ max_length=35,
+ verbose_name=_("IMEI")
+ )
+
+ PRICE_CATEGORIES = (
+ ('warranty', _("Warranty")),
+ ('exchange', _("Exchange Price")),
+ ('stock', _("Stock Price")),
+ )
+
+ price_category = models.CharField(
+ max_length=32,
+ choices=PRICE_CATEGORIES,
+ default=PRICE_CATEGORIES[0],
+ verbose_name=_("Price category")
+ )
+
+ comptia_code = models.CharField(
+ blank=True,
+ default="",
+ max_length=4,
+ verbose_name=_("symptom code")
+ )
+ comptia_modifier = models.CharField(
+ blank=True,
+ default="",
+ max_length=1,
+ verbose_name=_("symptom modifier")
+ )
+
+ def can_create_device(self):
+ pt = self.product.part_type
+ return pt == 'REPLACEMENT' and self.sn
+
+ def comptia_choices(self):
+ if self.product is not None:
+ from servo.models.parts import symptom_codes
+ return symptom_codes(self.product.component_code)
+
+ def get_comptia_code_display(self):
+ for i in self.comptia_choices():
+ if i[0] == self.comptia_code:
+ return i[1]
+
+ def get_comptia_modifier_display(self):
+ if self.comptia_modifier:
+ from servo.models.parts import symptom_modifiers
+ for m in symptom_modifiers():
+ if m[0] == self.comptia_modifier:
+ return m[1]
+
+ def get_part(self):
+ return self.servicepart_set.latest()
+
+ def get_poitem(self):
+ return self.purchaseorderitem_set.latest()
+
+ def get_repair(self):
+ return self.order.repair_set.latest()
+
+ def reserve_product(self):
+ location = self.order.location
+ inventory = Inventory.objects.get(location=location, product=self.product)
+ inventory.amount_reserved += self.amount
+ inventory.save()
+
+ def get_purchase_price(self):
+ """
+ Returns the purchase price of this SOIs Product
+ """
+ return self.product.get_price(self.price_category, "purchase")
+
+ def get_product_price(self, kind):
+ return self.product.get_price(category=self.price_category, kind=kind)
+
+ def save(self, *args, **kwargs):
+ self.sn = self.sn.upper()
+ self.kbb_sn = self.kbb_sn.upper()
+ return super(ServiceOrderItem, self).save(*args, **kwargs)
+
+ @property
+ def is_abused(self):
+ return self.price_category == 'stock'
+
+ @property
+ def is_warranty(self):
+ return self.price_category == 'warranty'
+
+ def __unicode__(self):
+ return self.code
+
+ class Meta:
+ app_label = "servo"
+ ordering = ('id',)
+ get_latest_by = "id"
+
+
+class OrderStatus(models.Model):
+ """
+ The M/M statuses of an order
+ """
+ order = models.ForeignKey(Order)
+ status = models.ForeignKey(Status)
+
+ started_at = models.DateTimeField(auto_now_add=True)
+ finished_at = models.DateTimeField(null=True)
+
+ started_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ related_name='+'
+ )
+ finished_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ related_name='+'
+ )
+
+ green_limit = models.DateTimeField(null=True)
+ yellow_limit = models.DateTimeField(null=True)
+
+ BADGES = (
+ ('undefined', 'undefined'),
+ ('success', 'success'),
+ ('warning', 'warning'),
+ ('danger', 'danger'),
+ )
+
+ badge = models.CharField(choices=BADGES, default=BADGES[0][0], max_length=16)
+ duration = models.IntegerField(default=0)
+
+ @classmethod
+ def create(cls, order, queue_status, user):
+ """
+ Set status or order to queue_status.status
+ """
+ new_status = queue_status.status
+ os = cls(order=order, status=new_status)
+ os.started_by = user
+ #os.started_at = timezone.now()
+
+ os.green_limit = queue_status.get_green_limit()
+ os.yellow_limit = queue_status.get_yellow_limit()
+
+ os.save()
+
+ prev = os.get_previous()
+
+ if prev is None:
+ return
+
+ # set color of previous OS
+ if prev.finished_by is None:
+ prev.finished_by = user
+ prev.finished_at = timezone.now()
+ prev.duration = (prev.finished_at - prev.started_at).total_seconds()
+
+ prev.badge = prev.get_badge()
+ prev.save()
+
+ def get_badge(self):
+ now = timezone.now()
+ badge = "undefined"
+ if self.yellow_limit and now > self.yellow_limit:
+ badge = "danger"
+ if self.yellow_limit and now < self.yellow_limit:
+ badge = "warning"
+ if self.green_limit and now < self.green_limit:
+ badge = "success"
+
+ return badge
+
+ def get_next(self):
+ statuses = self.order.orderstatus_set
+ return statuses.filter(started_at__gt=self.started_at).order_by('id').first()
+
+ def get_previous(self):
+ statuses = self.order.orderstatus_set
+ return statuses.filter(started_at__lt=self.started_at).order_by('id').last()
+
+ def __unicode__(self):
+ return self.status.title
+
+ class Meta:
+ app_label = "servo"
+ ordering = ('-started_at',)
+ get_latest_by = "started_at"
+
+
+class OrderDevice(models.Model):
+ """
+ A device attached to a service order
+ """
+ order = models.ForeignKey(Order)
+ device = models.ForeignKey(Device)
+ should_report = models.BooleanField(default=True)
+
+ def is_repeat_service(self):
+ from django.utils import timezone
+ created_at = self.order.created_at
+ tlimit = timezone.now() - timedelta(days=30)
+ orders = Order.objects.filter(orderdevice__device=self.device,
+ created_at__lt=created_at,
+ created_at__gte=tlimit)
+
+ return orders.exclude(pk=self.order.pk).count() > 0
+
+ class Meta:
+ app_label = "servo"
+ unique_together = ('order', 'device', ) # Can't add the same device more than once
+
+
+class Accessory(models.Model):
+ """
+ An accessory that came with the device in this Service Order
+ """
+ name = models.TextField()
+ qty = models.IntegerField(default=1)
+ device = models.ForeignKey(Device)
+ order = models.ForeignKey(Order)
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ app_label = "servo"
+
+
+@receiver(pre_save, sender=Order)
+def trigger_order_presave(sender, instance, **kwargs):
+ instance.customer_name = ''
+ if instance.customer is not None:
+ instance.customer_name = instance.customer.fullname
+
+ location = instance.created_by.location
+
+ if instance.checkin_location is None:
+ instance.checkin_location = location
+
+ if instance.location_id is None:
+ instance.location = location
+
+ if instance.checkout_location is None:
+ instance.checkout_location = location
+
+@receiver(post_save, sender=Order)
+def trigger_order_created(sender, instance, created, **kwargs):
+ if created:
+ instance.url_code = encode_url(instance.id).upper()
+ instance.code = settings.INSTALL_ID + str(instance.id).rjust(6, '0')
+ description = _('Order %s created') % instance.code
+ instance.notify('created', description, instance.created_by)
+ instance.save()
+
+
+@receiver(post_save, sender=OrderDevice)
+def trigger_orderdevice_saved(sender, instance, created, **kwargs):
+ order = instance.order
+ device = instance.device
+ order.description = device.description
+
+ if order.queue is None:
+ pass # @TODO try to autoasign case to right queue...
+
+ order.save()
+
+
+@receiver(post_delete, sender=OrderDevice)
+def trigger_device_removed(sender, instance, **kwargs):
+ try:
+ order = instance.order
+ except Order.DoesNotExist:
+ return # Means the whole order was deleted, not just the device
+ devices = order.devices.all()
+ if devices.count() > 0:
+ order.description = devices[0].description
+ else:
+ order.description = ''
+
+ order.save()
diff --git a/servo/models/parts.py b/servo/models/parts.py
new file mode 100644
index 0000000..c2d8e25
--- /dev/null
+++ b/servo/models/parts.py
@@ -0,0 +1,407 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import yaml
+import gsxws
+
+from django.db import models
+from django.utils import timezone
+from django.core.files import File
+from django.utils.translation import ugettext_lazy as _
+
+from servo.models.shipments import Shipment
+from servo.models.order import ServiceOrderItem
+from servo.models.purchases import PurchaseOrder, PurchaseOrderItem
+
+
+def symptom_modifiers():
+ return gsxws.MODIFIERS
+
+
+def symptom_codes(group):
+ """
+ Return symptom codes for component group
+ """
+ if group == '':
+ return
+
+ data = yaml.load(open("servo/fixtures/comptia.yaml", "r"))
+ symptoms = data[group]['symptoms']
+ srted = sorted(symptoms)
+ codes = [(k, "%s - %s " % (k, symptoms[k])) for k in srted]
+ return codes
+
+
+class ServicePart(models.Model):
+ """
+ Stores the data necessary to connect our ServiceOrderItems
+ with the corresponding GSX parts
+ """
+ repair = models.ForeignKey("Repair", editable=False)
+ order_item = models.ForeignKey(ServiceOrderItem, editable=False)
+ purchase_order = models.ForeignKey(PurchaseOrder, null=True, editable=False)
+
+ comptia_code = models.CharField(
+ max_length=4,
+ editable=False,
+ verbose_name=_("Symptom Code")
+ )
+ comptia_modifier = models.CharField(
+ max_length=1,
+ editable=False,
+ verbose_name=_("Symptom Modifier")
+ )
+
+ # maps to partsInfo/orderLineNumber
+ line_number = models.SmallIntegerField(null=True, editable=False)
+ registered_for_return = models.BooleanField(default=False)
+ returned_at = models.DateTimeField(null=True, editable=False)
+
+ ship_to = models.CharField(max_length=18, editable=False)
+ part_title = models.CharField(max_length=128)
+ part_number = models.CharField(max_length=18)
+ service_order = models.CharField(max_length=10)
+ return_order = models.CharField(max_length=10, default='')
+
+ # maps Return Status (Known Bad Board)
+ return_status = models.CharField(default='', max_length=128, editable=False)
+ # maps Return Code (KBB, NRET)
+ return_code = models.CharField(default='', max_length=4, editable=False)
+ # maps to GSX Order Status
+ order_status = models.CharField(default='', max_length=128, editable=False)
+ # maps to GSX Order Status Code
+ order_status_code = models.CharField(default='', max_length=4, editable=False)
+
+ COVERAGE_STATUS_CHOICES = (
+ ('CC', _('Custom Bid Contracts')),
+ ('CS', _('Customer Satisfaction')),
+ ('DO', _('DOA Coverage')),
+ ('LI', _('Apple Limited Warranty')),
+ ('MU', _('Missing Upon First Use')),
+ ('OO', _('Out of Warranty (No Coverage)')),
+ ('PA', _('AppleCare Parts Agreement')),
+ ('PP', _('AppleCare Protection Plan')),
+ ('QP', _('Quality Program')),
+ ('RA', _('AppleCare Repair Agreement')),
+ ('RE', _('Repeat Service')),
+ ('PT', _('Additional Part Coverage')),
+ ('EC', _('Additional Service Coverage')),
+ ('C1', _('NEW - AppleCare Protection Plan')),
+ ('VW', _('Consumer Law Coverage')),
+ )
+ """
+ coverage_status = models.CharField(
+ default='',
+ max_length=3,
+ choices=COVERAGE_STATUS_CHOICES
+ )
+ """
+ coverage_description = models.CharField(
+ default='',
+ max_length=128,
+ editable=False
+ )
+
+ shipment = models.ForeignKey(Shipment, null=True)
+ box_number = models.PositiveIntegerField(null=True)
+ return_label = models.FileField(
+ null=True,
+ editable=False,
+ upload_to="return_labels"
+ )
+ carrier_url = models.CharField(default='', max_length=255, editable=False)
+
+ def get_symptom_code_display(self):
+ codes = self.get_comptia_symptoms() or []
+ try:
+ return [c[1] for c in codes if c[0] == self.comptia_code][0]
+ except IndexError:
+ return self.comptia_code
+
+ def get_symptom_modifier_display(self):
+ mods = symptom_modifiers()
+ try:
+ return [m[1] for m in mods if m[0] == self.comptia_modifier][0]
+ except IndexError:
+ return self.comptia_modifier
+
+ @property
+ def reference(self):
+ return self.repair.reference
+
+ @classmethod
+ def from_soi(cls, repair, soi):
+ """
+ Creates and returns a ServicePart from a repair and ServiceOrderItem
+ """
+ part = cls(repair=repair, order_item=soi)
+ part.part_title = soi.title
+ part.part_number = soi.code
+ part.service_order = soi.order.code
+ part.ship_to = repair.gsx_account.ship_to
+ part.comptia_code = soi.comptia_code
+ part.comptia_modifier = soi.comptia_modifier
+ return part
+
+ def order(self, user, po=None):
+ """
+ Purchase this Service Part
+ """
+ if po is None:
+ po = PurchaseOrder()
+ po.location = user.get_location()
+ po.sales_order = self.repair.order
+ po.reference = self.repair.reference
+ po.confirmation = self.repair.confirmation
+ po.created_by = user
+ po.supplier = "Apple"
+ po.save()
+
+ self.purchase_order = po
+ poi = PurchaseOrderItem(purchase_order=po)
+ poi.code = self.part_number
+ poi.title = self.part_title
+ poi.order_item = self.order_item
+ poi.product = self.order_item.product
+ poi.price = self.order_item.get_purchase_price()
+
+ poi.save()
+
+ if po.submitted_at is None:
+ po.submit(user)
+
+ self.save()
+
+ def is_replacement(self):
+ return self.order_item.product.part_type == 'REPLACEMENT'
+
+ def mark_doa(self):
+ """
+ Marking a part DOA means we get a new part, so:
+ - make a copy of the old part
+ """
+ # Update our PO so we know to expect the replacement for the DOA part
+ poi = PurchaseOrderItem(price=0, purchase_order=self.purchase_order)
+ poi.product = self.order_item.product
+ poi.order_item = self.order_item
+ poi.ordered_at = timezone.now()
+ poi.save()
+
+ # Create a copy of this part and reset
+ new_part = self
+ new_part.pk = None
+ new_part.shipment = None
+ new_part.line_number = None
+ new_part.returned_at = None
+
+ new_part.return_order = ''
+ new_part.order_status = ''
+ new_part.return_label = None
+ new_part.order_status_code = ''
+ new_part.coverage_description = ''
+ new_part.registered_for_return = False
+
+ new_part.save()
+
+ def set_part_details(self, gsx_part):
+ """
+ Updates this part to match the info from gsx_part
+ """
+ self.comptia_code = gsx_part.comptiaCode or ''
+ self.return_order = gsx_part.returnOrderNumber or ''
+ self.comptia_modifier = gsx_part.comptiaModifier or ''
+
+ self.order_status = gsx_part.orderStatus or ''
+ self.order_status_code = gsx_part.orderStatusCode or ''
+ self.coverage_description = gsx_part.partCoverageDescription or ''
+
+ self.return_code = gsx_part.returnCode or ''
+ self.return_status = gsx_part.returnStatus or ''
+ self.carrier_url = gsx_part.carrierURL or ''
+ self.line_number = gsx_part.orderLineNumber
+
+ return self
+
+ def update_part(self, return_data, return_type, user):
+ """
+ gsx/returns/Parts Return Update
+ """
+ self.repair.connect_gsx(user)
+
+ p = {'partNumber': self.part_number, 'returnType': return_type}
+ p.update(return_data)
+ part = gsxws.RepairOrderLine(**p)
+ ret = gsxws.Return()
+
+ ret.update_parts(self.repair.confirmation, [part])
+
+ if return_type == Shipment.RETURN_DOA:
+ self.mark_doa()
+
+ def can_return(self):
+ return not self.return_order == ''
+
+ def get_return_title(self):
+ if self.registered_for_return:
+ return _("Unregister from Return")
+
+ return _("Register for Return")
+
+ def register_for_return(self, user):
+ """
+ Registers this part for the current bulk return
+ """
+ location = user.get_location()
+ ship_to = self.repair.gsx_account.ship_to
+ shipment = Shipment.get_current(user, location, ship_to)
+ shipment.toggle_part(self)
+
+ def to_gsx(self):
+ """
+ Returns a GSX ServicePart entry for this part
+ """
+ part = gsxws.ServicePart(self.part_number)
+ part.returnOrderNumber = self.return_order
+ if self.box_number > 0:
+ part.boxNumber = self.box_number
+ return part
+
+ def needs_comptia_code(self):
+ """
+ CompTIA not required for Replacement and Other category parts.
+ In practice this is here for Adjustment-type parts (#011-0663)
+ """
+ return self.order_item.product.part_type != 'ADJUSTMENT'
+
+ def get_repair_order_line(self):
+ """
+ Returns GSX RepairOrderLine entry for this part
+ """
+ ol = gsxws.RepairOrderLine()
+ ol.partNumber = self.part_number
+
+ oi = self.order_item
+ ol.abused = oi.is_abused
+
+ if self.needs_comptia_code():
+ ol.comptiaCode = self.comptia_code
+ ol.comptiaModifier = self.comptia_modifier
+
+ device = self.repair.device
+
+ # Warranty only when warranty price (0) and no damage
+ # warranty means outOfWarrantyFlag=False
+ if device and not oi.is_abused:
+ if device.has_warranty:
+ ol.returnableDamage = not oi.is_warranty
+
+ return ol
+
+ def get_comptia_symptoms(self):
+ """
+ Returns the appropriate CompTIA codes for this part
+ """
+ product = self.order_item.product
+ return symptom_codes(product.component_code)
+
+ def get_return_label(self):
+ """
+ Returns the GSX return label for this part
+ """
+ if self.return_label.name == "":
+ # Return label not yet set, get it...
+ label = gsxws.Return(self.return_order).get_label(self.part_number)
+ filename = "%s_%s.pdf" % (self.return_order, self.part_number)
+
+ f = File(open(label.returnLabelFileData))
+ self.return_label = f
+ self.save()
+ self.return_label.save(filename, f)
+
+ return self.return_label.read()
+
+ def update_module_sn(self):
+ """
+ Updates the GSX module serial numbers
+ """
+ part = gsxws.ServicePart(self.order_item.code)
+ part.oldSerialNumber = self.order_item.kbb_sn
+ part.serialNumber = self.order_item.sn
+ part.reasonCode = "OT"
+ part.isPartDOA = "N"
+ repair = self.repair.get_gsx_repair()
+ return repair.update_sn([part])
+
+ def update_replacement_sn(self):
+ """
+ Updates the Whole-Unit swap KGB SN
+ With the user's own GSX credentials, falling back to the defaults
+ """
+ repair = self.repair.get_gsx_repair()
+ return repair.update_kgb_sn(self.order_item.sn)
+
+ def can_update_sn(self):
+ soi = self.order_item
+ return not soi.sn == ''
+
+ def update_sn(self):
+ # CTS parts not eligible for SN update
+ if self.return_status == 'Convert To Stock':
+ return
+
+ if not self.repair.confirmation:
+ raise ValueError(_('GSX repair has no dispatch ID'))
+
+ product = self.order_item.product
+
+ if not product.is_serialized:
+ return
+
+ if product.part_type == "MODULE":
+ self.update_module_sn()
+ elif product.part_type == "REPLACEMENT":
+ self.update_replacement_sn()
+
+ def lookup(self):
+ return gsxws.Part(partNumber=self.part_number).lookup()
+
+ def save(self, *args, **kwargs):
+ if self.comptia_code is None:
+ oi = self.order_item
+ self.comptia_code = oi.comptia_code
+ self.comptia_modifier = oi.comptia_modifier
+
+ super(ServicePart, self).save(*args, **kwargs)
+
+ def __unicode__(self):
+ return u'ServicePart %s' % self.part_number
+
+ class Meta:
+ app_label = "servo"
+ get_latest_by = "id"
+ ordering = ("order_item",)
+ # A part can only appear once per shipment
+ unique_together = ("id", "shipment",)
diff --git a/servo/models/product.py b/servo/models/product.py
new file mode 100644
index 0000000..b3bd787
--- /dev/null
+++ b/servo/models/product.py
@@ -0,0 +1,654 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+from os.path import basename
+
+from django.db import models
+from django.db import connection
+from django.conf import settings
+from django.core.files import File
+from django.core.cache import cache
+from decimal import Decimal, ROUND_CEILING
+from django.core.urlresolvers import reverse
+
+from django.contrib.contenttypes.fields import GenericRelation
+
+from django.contrib.sites.managers import CurrentSiteManager
+
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+
+from mptt.models import MPTTModel, TreeForeignKey
+from mptt.managers import TreeManager
+from gsxws import comptia, parts, validate
+
+from servo import defaults
+from servo.lib.shorturl import from_time
+from servo.models import Configuration, Location, TaggedItem
+
+
+def get_margin(price=0.0):
+ """
+ Returns the proper margin % for this price
+ """
+ price = Decimal(price)
+ margin = defaults.margin()
+
+ try:
+ return Decimal(margin)
+ except Exception:
+ ranges = margin.split(';')
+ for r in ranges:
+ m = re.search(r'(\d+)\-(\d+)=(\d+)', r)
+ p_min, p_max, margin = m.groups()
+ if Decimal(p_min) <= price <= Decimal(p_max):
+ return Decimal(margin)
+
+ return Decimal(margin)
+
+
+def default_vat():
+ conf = Configuration.conf()
+ return conf.get("pct_vat", 0.0)
+
+
+def inventory_totals():
+ """
+ Returns the total purchase and sales value
+ of our inventory
+ """
+ cursor = connection.cursor()
+ sql = """SELECT SUM(price_purchase_stock*total_amount) AS a,
+ SUM(price_sales_stock*total_amount) AS b
+ FROM servo_product
+ WHERE part_type != 'SERVICE'"""
+ cursor.execute(sql)
+
+ for k, v in cursor.fetchall():
+ return (k, v)
+
+
+class DeviceGroup(models.Model):
+ """
+ This links products with devices.
+ The title should match a device's description field
+ """
+ title = models.CharField(max_length=128, unique=True)
+
+ class Meta:
+ app_label = "servo"
+
+
+class AbstractBaseProduct(models.Model):
+ code = models.CharField(
+ unique=True,
+ max_length=32,
+ default=from_time,
+ verbose_name=_("code")
+ )
+
+ subst_code = models.CharField(
+ default='',
+ max_length=32,
+ editable=False,
+ verbose_name=_("Substituted (new) code of this part")
+ )
+
+ title = models.CharField(
+ max_length=255,
+ default=_("New Product"),
+ verbose_name=_("Title")
+ )
+ description = models.TextField(
+ default='',
+ blank=True,
+ verbose_name=_("Description")
+ )
+ pct_vat = models.DecimalField(
+ max_digits=4,
+ decimal_places=2,
+ default=default_vat,
+ verbose_name=_("VAT %")
+ )
+
+ fixed_price = models.BooleanField(
+ default=False,
+ help_text=_("Don't update price when recalculating prices or importing parts")
+ )
+
+ price_purchase_exchange = models.DecimalField(
+ default=0,
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Purchase price")
+ )
+
+ pct_margin_exchange = models.DecimalField(
+ max_digits=4,
+ decimal_places=2,
+ default=get_margin,
+ verbose_name=_("Margin %")
+ )
+ price_notax_exchange = models.DecimalField(
+ default=0,
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Net price"),
+ help_text=_("Purchase price + margin %")
+ )
+ price_sales_exchange = models.DecimalField(
+ default=0,
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Sales price"),
+ help_text=_("Purchase price + margin % + shipping + VAT %")
+ )
+
+ price_purchase_stock = models.DecimalField(
+ default=0,
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Purchase price")
+ )
+ pct_margin_stock = models.DecimalField(
+ max_digits=4,
+ decimal_places=2,
+ default=get_margin,
+ verbose_name=_("Margin %")
+ )
+ price_notax_stock = models.DecimalField(
+ default=0,
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Net price"),
+ help_text=_("Purchase price + margin %")
+ )
+ price_sales_stock = models.DecimalField(
+ default=0,
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Sales price"),
+ help_text=_("Purchase price + margin % + shipping + VAT %")
+ )
+
+ is_serialized = models.BooleanField(
+ default=False,
+ verbose_name=_('is serialized'),
+ help_text=_("Product has a serial number")
+ )
+
+ class Meta:
+ app_label = 'servo'
+ abstract = True
+
+
+class Product(AbstractBaseProduct):
+
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ warranty_period = models.PositiveIntegerField(
+ default=0,
+ verbose_name=_("Warranty (months)")
+ )
+ shelf = models.CharField(
+ default='',
+ blank=True,
+ max_length=8,
+ verbose_name=_("Shelf code")
+ )
+ brand = models.CharField(
+ default='',
+ blank=True,
+ max_length=32,
+ verbose_name=_("Brand")
+ )
+ categories = models.ManyToManyField(
+ "ProductCategory",
+ blank=True,
+ verbose_name=_("Categories")
+ )
+ device_models = models.ManyToManyField(
+ "DeviceGroup",
+ blank=True,
+ verbose_name=_("device models")
+ )
+ tags = GenericRelation(TaggedItem)
+ photo = models.ImageField(
+ null=True,
+ blank=True,
+ upload_to="products",
+ verbose_name=_("photo")
+ )
+
+ shipping = models.FloatField(default=0, verbose_name=_('shipping'))
+
+ # component code is used to identify Apple parts
+ component_code = models.CharField(
+ blank=True,
+ default='',
+ max_length=1,
+ choices=comptia.GROUPS,
+ verbose_name=_("component group")
+ )
+
+ labour_tier = models.CharField(max_length=15, blank=True, default='')
+
+ # We need this to call the correct GSX SN Update API
+ PART_TYPES = (
+ ('ADJUSTMENT', _("Adjustment")),
+ ('MODULE', _("Module")),
+ ('REPLACEMENT', _("Replacement")),
+ ('SERVICE', _("Service")),
+ ('SERVICE CONTRACT', _("Service Contract")),
+ ('OTHER', _("Other")),
+ )
+
+ part_type = models.CharField(
+ max_length=18,
+ default='OTHER',
+ choices=PART_TYPES,
+ verbose_name=_("part type")
+ )
+
+ eee_code = models.CharField(
+ blank=True,
+ default='',
+ max_length=256,
+ verbose_name=_("EEE code")
+ )
+
+ total_amount = models.IntegerField(editable=False, default=0)
+
+ def get_pick_url(self, order, device=None):
+ pk = self.pk or self.code
+ return '/orders/%d/devices/%d/'
+
+ def is_service(self):
+ return self.part_type == 'SERVICE'
+
+ def get_warranty_display(self):
+ if self.warranty_period:
+ return _("%d months") % self.warranty_period
+
+ def can_order_from_gsx(self):
+ return self.component_code and self.part_type in ("MODULE", "REPLACEMENT", "OTHER",)
+
+ def can_update_price(self):
+ return self.can_order_from_gsx() and not self.fixed_price
+
+ def update_price(self, new_product=None):
+ """
+ Updates part's price info from GSX or to match new_product
+ """
+ if new_product is None:
+ part = parts.Part(partNumber=self.code).lookup()
+ new_product = Product.from_gsx(part)
+
+ self.price_purchase_exchange = new_product.price_purchase_exchange
+ self.price_purchase_stock = new_product.price_purchase_stock
+
+ self.title = new_product.title
+
+ self.component_code = new_product.component_code
+
+ self.price_notax_stock = new_product.price_notax_stock
+ self.price_notax_exchange = new_product.price_notax_exchange
+
+ self.pct_margin_stock = new_product.pct_margin_stock
+ self.pct_margin_exchange = new_product.pct_margin_exchange
+
+ self.price_sales_stock = new_product.price_sales_stock
+ self.price_sales_exchange = new_product.price_sales_exchange
+
+ self.save()
+
+ def calculate_price(self, price, shipping=0.0):
+ """
+ Calculates price and returns it w/ and w/o tax
+ """
+ conf = Configuration.conf()
+ shipping = shipping or 0.0
+
+ if not isinstance(shipping, Decimal):
+ shipping = Decimal(shipping)
+
+ margin = get_margin(price)
+ vat = Decimal(conf.get("pct_vat", 0.0))
+
+ # TWOPLACES = Decimal(10) ** -2 # same as Decimal('0.01')
+ # @TODO: make rounding configurable!
+ wo_tax = ((price*100)/(100-margin)+shipping).to_integral_exact(rounding=ROUND_CEILING)
+ with_tax = (wo_tax*(vat+100)/100).to_integral_exact(rounding=ROUND_CEILING)
+
+ return wo_tax, with_tax
+
+ def set_stock_sales_price(self):
+ if not self.price_purchase_stock or self.fixed_price:
+ return
+
+ purchase_sp = self.price_purchase_stock
+ sp, vat_sp = self.calculate_price(purchase_sp, self.shipping)
+ self.price_notax_stock = sp
+ self.price_sales_stock = vat_sp
+
+ def set_exchange_sales_price(self):
+ if not self.price_purchase_exchange or self.fixed_price:
+ return
+
+ purchase_ep = self.price_purchase_exchange
+ ep, vat_ep = self.calculate_price(purchase_ep, self.shipping)
+ self.price_notax_exhcange = ep
+ self.price_sales_exchange = vat_ep
+
+ @property
+ def is_apple_part(self):
+ return validate(self.code, 'partNumber')
+
+ @classmethod
+ def from_gsx(cls, part):
+ """
+ Creates a Servo Product from GSX partDetail.
+ We don't do GSX lookups here since we can't
+ determine the correct GSX Account at this point.
+ """
+ conf = Configuration.conf()
+
+ try:
+ shipping = Decimal(conf.get("shipping_cost"))
+ except TypeError:
+ shipping = Decimal(0.0)
+
+ part_number = part.originalPartNumber or part.partNumber
+ product = Product(code=part_number)
+ product.title = part.partDescription
+
+ if part.originalPartNumber:
+ product.subst_code = part.partNumber
+
+ if part.stockPrice and not product.fixed_price:
+ # calculate stock price
+ purchase_sp = part.stockPrice or 0.0
+ purchase_sp = Decimal(purchase_sp)
+ sp, vat_sp = product.calculate_price(purchase_sp, shipping)
+ product.pct_margin_stock = get_margin(purchase_sp)
+ product.price_notax_stock = sp
+ product.price_sales_stock = vat_sp
+ # @TODO: make rounding configurable
+ product.price_purchase_stock = purchase_sp.to_integral_exact(
+ rounding=ROUND_CEILING
+ )
+
+ try:
+ # calculate exchange price
+ purchase_ep = part.exchangePrice or 0.0
+ purchase_ep = Decimal(purchase_ep)
+
+ if purchase_ep > 0 and not product.fixed_price:
+ ep, vat_ep = product.calculate_price(purchase_ep, shipping)
+ product.price_notax_exchange = ep
+ product.price_sales_exchange = vat_ep
+ product.pct_margin_exchange = Configuration.get_margin(purchase_ep)
+ # @TODO: make rounding configurable
+ product.price_purchase_exchange = purchase_ep.to_integral_exact(
+ rounding=ROUND_CEILING
+ )
+ except AttributeError:
+ pass # Not all parts have exchange prices
+
+ product.brand = "Apple"
+ product.shipping = shipping
+ product.warranty_period = 3
+
+ product.labour_tier = part.laborTier
+ product.part_type = part.partType.upper()
+
+ # eee and componentCode are sometimes missing
+ if part.eeeCode:
+ product.eee_code = str(part.eeeCode).strip()
+ if part.componentCode:
+ product.component_code = str(part.componentCode).strip()
+
+ product.is_serialized = part.isSerialized
+ return product
+
+ @classmethod
+ def from_cache(cls, code):
+ data = cache.get(code)
+ return cls.from_gsx(data)
+
+ def get_photo(self):
+ try:
+ return self.photo.url
+ except ValueError:
+ from django.conf import settings
+ return "%simages/na.gif" % settings.STATIC_URL
+
+ def tax(self):
+ return self.price_sales - self.price_notax
+
+ def latest_date_sold(self):
+ return '-'
+
+ def latest_date_ordered(self):
+ return '-'
+
+ def latest_date_arrived(self):
+ return '-'
+
+ def sell(self, amount, location):
+ """
+ Deduct product from inventory with specified location
+ """
+ track_inventory = Configuration.track_inventory()
+
+ if self.part_type == "SERVICE" or not track_inventory:
+ return
+
+ try:
+ inventory = Inventory.objects.get(product=self, location=location)
+ inventory.amount_stocked = inventory.amount_stocked - amount
+ inventory.save()
+ except Inventory.DoesNotExist:
+ raise ValueError(_(u"Product %s not found in inventory.") % self.code)
+
+ def get_relative_url(self):
+ if self.pk is None:
+ return "code/%s/" % self.code
+
+ return self.pk
+
+ def get_absolute_url(self):
+ if self.pk is None:
+ return reverse("products-view_product", kwargs={'code': self.code})
+ return reverse("products-view_product", kwargs={'pk': self.pk})
+
+ def get_amount_stocked(self, user):
+ """
+ Returns the amount of this product in the same location as the user.
+ Caches the result for faster access later.
+ """
+ amount = 0
+ track_inventory = Configuration.track_inventory()
+
+ if not track_inventory:
+ return 0
+
+ if self.part_type == "SERVICE" or not self.pk:
+ return 0
+
+ cache_key = "product_%d_amount_stocked" % self.pk
+
+ if cache.get(cache_key):
+ return cache.get(cache_key)
+
+ location = user.get_location()
+
+ try:
+ inventory = Inventory.objects.get(product=self, location=location)
+ amount = inventory.amount_stocked
+ except Inventory.DoesNotExist:
+ pass
+
+ cache.set(cache_key, amount)
+ return amount
+
+ def get_price(self, category=None, kind="sales"):
+ """
+ Returns price of product in specific price category
+ of the specified kind (sales or purchase)
+ """
+ prices = dict(
+ warranty=0.0,
+ exchange=float(getattr(self, "price_%s_exchange" % kind)),
+ stock=float(getattr(self, "price_%s_stock" % kind))
+ )
+
+ return prices.get(category) if category else prices
+
+ def get_pk(self):
+ return self.pk or Product.objects.get(code=self.code).pk
+
+ def update_photo(self):
+ if self.component_code and not self.photo:
+ try:
+ part = parts.Part(partNumber=self.code)
+ result = part.fetch_image()
+ filename = basename(result)
+ self.photo.save(filename, File(open(result)))
+ except Exception, e:
+ print e
+
+ def __unicode__(self):
+ return u'%s %s' % (self.code, self.title)
+
+ class Meta:
+ ordering = ('-id',)
+ app_label = 'servo'
+ permissions = (
+ ("change_amount", _("Can change product amount")),
+ )
+
+
+class ProductCategory(MPTTModel):
+
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ title = models.CharField(max_length=255, default=_("New Category"))
+ slug = models.SlugField(null=True, editable=False)
+ parent = TreeForeignKey(
+ 'self',
+ null=True,
+ blank=True,
+ related_name='children'
+ )
+
+ objects = TreeManager()
+ on_site = CurrentSiteManager()
+
+ def get_products(self):
+ return Product.objects.filter(
+ categories__lft__gte=self.lft,
+ categories__rght__lte=self.rght,
+ categories__tree_id=self.tree_id
+ )
+
+ def get_product_count(self):
+ count = self.product_set.count()
+ return count if count > 0 else ""
+
+ def get_absolute_url(self):
+ return "/sales/products/%s/" % self.slug
+
+ def save(self, *args, **kwargs):
+ from django.utils.text import slugify
+ self.slug = slugify(self.title[:50])
+ return super(ProductCategory, self).save(*args, **kwargs)
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ app_label = "servo"
+ get_latest_by = "id"
+ ordering = ("-title",)
+ unique_together = ("title", "site", )
+
+
+class Inventory(models.Model):
+ """
+ Inventory tracks how much of Product X is in Location Y
+ """
+ product = models.ForeignKey(Product)
+ location = models.ForeignKey(Location)
+
+ amount_minimum = models.PositiveIntegerField(
+ default=0,
+ verbose_name=_("minimum amount")
+ )
+ amount_reserved = models.PositiveIntegerField(
+ default=0,
+ verbose_name=_("reserved amount")
+ )
+ amount_stocked = models.IntegerField(
+ default=0,
+ verbose_name=_("stocked amount")
+ )
+ amount_ordered = models.PositiveIntegerField(
+ default=0,
+ verbose_name=_("ordered amount")
+ )
+
+ def save(self, *args, **kwargs):
+ super(Inventory, self).save(*args, **kwargs)
+ total_amount = 0
+ for i in self.product.inventory_set.all():
+ total_amount += i.amount_stocked
+ self.product.total_amount = total_amount
+ self.product.save()
+
+ class Meta:
+ app_label = "servo"
+ unique_together = ('product', 'location',)
+
+
+class ShippingMethod(models.Model):
+ """
+ How the contents of an order should be shipped
+ """
+ title = models.CharField(max_length=128, default=_('New Shipping Method'))
+ description = models.TextField(default='', blank=True)
+ notify_email = models.EmailField(null=True, blank=True)
+
+ class Meta:
+ app_label = "servo"
diff --git a/servo/models/purchases.py b/servo/models/purchases.py
new file mode 100644
index 0000000..4b9a886
--- /dev/null
+++ b/servo/models/purchases.py
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.db import models
+from django.conf import settings
+from django.utils import timezone
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+
+from django.dispatch import receiver
+from django.db.models.signals import post_save
+
+from servo import defaults
+from servo.models.common import Location, Configuration
+from servo.models.product import Product, Inventory
+from servo.models.order import Order, AbstractOrderItem, ServiceOrderItem
+
+
+class PurchaseOrder(models.Model):
+ """
+ A purchase order(PO) consists of different purchase order items
+ all of which may reference individual Service Orders.
+ When a PO is submitted, the included items are registered
+ to the /products/incoming/ list (items that have not yet arrived).
+ A PO cannot be edited after it's been submitted.
+
+ Creating a PO from an SO only creates the PO, it does not submit it.
+ """
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ location = models.ForeignKey(
+ Location,
+ editable=False,
+ help_text=_('The location from which this PO was created')
+ )
+ sales_order = models.ForeignKey(Order, null=True, editable=False)
+ reference = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_("reference"),
+ )
+ confirmation = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_("confirmation"),
+ )
+
+ created_by = models.ForeignKey(settings.AUTH_USER_MODEL, editable=False)
+ created_at = models.DateTimeField(auto_now_add=True, editable=False)
+ submitted_at = models.DateTimeField(null=True, editable=False)
+
+ supplier = models.CharField(
+ blank=True,
+ max_length=32,
+ verbose_name=_("supplier")
+ )
+ carrier = models.CharField(
+ blank=True,
+ max_length=32,
+ verbose_name=_("carrier")
+ )
+ tracking_id = models.CharField(
+ blank=True,
+ max_length=128,
+ verbose_name=_("tracking ID")
+ )
+ days_delivered = models.IntegerField(
+ blank=True,
+ default=1,
+ verbose_name=_("delivery Time")
+ )
+
+ has_arrived = models.BooleanField(default=False)
+ total = models.FloatField(null=True, editable=False)
+ invoice_id = models.CharField(default='', max_length=10, editable=False)
+ invoice = models.FileField(
+ null=True,
+ editable=False,
+ upload_to="gsx_invoices",
+ help_text="Apple's sales invoice for this PO"
+ )
+
+ def only_apple_parts(self):
+ for p in self.purchaseorderitem_set.all():
+ if not p.product.is_apple_part:
+ return False
+ return True
+
+ @property
+ def is_editable(self):
+ return self.submitted_at is None
+
+ def can_create_gsx_stock(self):
+ return self.is_editable and self.confirmation == ''
+
+ def order_items(self, items):
+ pass
+
+ def get_absolute_url(self):
+ from django.core.urlresolvers import reverse
+ if self.submitted_at:
+ return reverse("purchases-view_po", args=[self.pk])
+ return reverse("purchases-edit_po", args=[self.pk])
+
+ def sum(self):
+ total = 0
+ for p in self.purchaseorderitem_set.all():
+ total += float(p.price*p.amount)
+
+ return total
+
+ def amount(self):
+ amount = 0
+ for p in self.purchaseorderitem_set.all():
+ amount += p.amount
+
+ return amount
+
+ def submit(self, user):
+ "Submits this Purchase Order"
+ if self.submitted_at is not None:
+ raise ValueError(_("Purchase Order %d has already been submitted") % self.pk)
+
+ location = user.get_location()
+
+ for i in self.purchaseorderitem_set.all():
+ inventory = Inventory.objects.get_or_create(location=location,
+ product=i.product)[0]
+ inventory.amount_ordered += i.amount
+ inventory.save()
+ i.ordered_at = timezone.now()
+ i.save()
+
+ self.submitted_at = timezone.now()
+ self.save()
+
+ def cancel(self):
+ """
+ Cancels this Purchase Order
+ Declined Repairs etc
+ """
+ location = self.created_by.get_location()
+
+ for i in self.purchaseorderitem_set.all():
+ inventory = Inventory.objects.get(location=location, product=i.product)
+ inventory.amount_ordered -= i.amount
+ inventory.save()
+ i.expected_ship_date = None
+ i.save()
+
+ def add_product(self, product, amount, user):
+ """
+ Adds a product to this Purchase Order
+ """
+ poi = PurchaseOrderItem(amount=amount, purchase_order=self)
+ poi.created_by = user
+ # adding from a Service Order
+ if isinstance(product, AbstractOrderItem):
+ poi.code = product.product.code
+ poi.order_item = product
+ poi.price = product.price
+ poi.product_id = product.product.id
+ poi.title = product.product.title
+ # adding from Stock
+ if isinstance(product, Product):
+ poi.code = product.code
+ poi.title = product.title
+ poi.product_id = product.id
+ poi.price = product.price_purchase_stock
+
+ poi.save()
+
+ def delete(self, *args, **kwargs):
+ if self.submitted_at:
+ raise ValueError(_('Submitted orders cannot be deleted'))
+ return super(PurchaseOrder, self).delete(*args, **kwargs)
+
+ class Meta:
+ ordering = ('-id',)
+ app_label = 'servo'
+
+
+class PurchaseOrderItem(AbstractOrderItem):
+ "An item being purchased"
+ price = models.DecimalField(
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Purchase Price"),
+ help_text=_("Purchase price without taxes")
+ )
+
+ purchase_order = models.ForeignKey(
+ PurchaseOrder,
+ editable=False,
+ verbose_name=_("Purchase Order")
+ )
+
+ order_item = models.ForeignKey(ServiceOrderItem, null=True, editable=False)
+ reference = models.CharField(default='', blank=True, max_length=128)
+ ordered_at = models.DateTimeField(null=True, editable=False)
+
+ expected_ship_date = models.DateField(null=True, editable=False)
+ received_at = models.DateTimeField(
+ null=True,
+ blank=True,
+ editable=False,
+ verbose_name=_("arrived")
+ )
+
+ received_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ editable=False,
+ related_name='+'
+ )
+
+ @classmethod
+ def from_soi(cls, po, soi, user):
+ """
+ Creates a new POI from a Sales Order item
+ """
+ poi = cls(purchase_order=po, order_item=soi)
+ poi.code = soi.code
+ poi.title = soi.title
+ poi.created_by = user
+
+ poi.price = soi.get_purchase_price()
+ poi.product = soi.product
+
+ return poi
+
+ def get_incoming_url(self):
+ """
+ Returns the correct URL to receive this item
+ """
+ if self.received_at is None:
+ date = "0000-00-00"
+ else:
+ date = self.received_at.strftime("%Y-%m-%d")
+
+ return "/sales/shipments/incoming/%s/%d/" % (date, self.pk)
+
+ def receive(self, user):
+ if self.received_at is not None:
+ raise ValueError(_("Product has already been received"))
+ self.received_at = timezone.now()
+ self.received_by = user
+ self.save()
+
+ def save(self, *args, **kwargs):
+ super(PurchaseOrderItem, self).save(*args, **kwargs)
+ # Sync SOI and POI serial numbers
+ if self.order_item:
+ if self.order_item.sn and not self.sn:
+ self.sn = self.order_item.sn
+ else:
+ self.order_item.sn = self.sn
+
+ self.order_item.save()
+
+ class Meta:
+ ordering = ('id',)
+ app_label = 'servo'
+ get_latest_by = 'id'
+
+
+@receiver(post_save, sender=PurchaseOrderItem)
+def trigger_product_received(sender, instance, created, **kwargs):
+
+ if instance.received_at is None:
+ return
+
+ product = instance.product
+ po = instance.purchase_order
+ location = po.created_by.get_location()
+
+ inventory = Inventory.objects.get_or_create(location=location, product=product)[0]
+
+ # Receiving an incoming item
+ if Configuration.track_inventory():
+ try:
+ inventory.amount_ordered -= instance.amount
+ inventory.amount_stocked += instance.amount
+ inventory.save()
+ except Exception:
+ ref = po.reference or po.confirmation
+ ed = {'prod': product.code, 'ref': ref}
+ raise ValueError(_('Cannot receive item %(prod)s (%(ref)s)') % ed)
+
+ sales_order = instance.purchase_order.sales_order
+
+ if sales_order is None:
+ return
+
+ # Trigger status change for parts receive
+ if sales_order.queue:
+ new_status = sales_order.queue.status_products_received
+ if new_status and sales_order.is_editable:
+ user = instance.received_by or instance.created_by
+ sales_order.set_status(new_status, user)
+
+
+@receiver(post_save, sender=PurchaseOrder)
+def trigger_purchase_order_created(sender, instance, created, **kwargs):
+
+ sales_order = instance.sales_order
+
+ if sales_order is None:
+ return
+
+ if not sales_order.is_editable:
+ return
+
+ if created:
+ msg = _("Purchase Order %d created") % instance.id
+ sales_order.notify("po_created", msg, instance.created_by)
+
+ # Trigger status change for GSX repair submit (if defined)
+ if instance.submitted_at:
+ if sales_order.queue:
+ queue = sales_order.queue
+ if queue.status_products_ordered:
+ # Queue has a status for product_ordered - trigger it
+ new_status = queue.status_products_ordered
+ sales_order.set_status(new_status, instance.created_by)
diff --git a/servo/models/queue.py b/servo/models/queue.py
new file mode 100644
index 0000000..d3eb0f6
--- /dev/null
+++ b/servo/models/queue.py
@@ -0,0 +1,296 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from datetime import timedelta
+from django.conf import settings
+
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.sites.models import Site
+from django.contrib.sites.managers import CurrentSiteManager
+from django.core.urlresolvers import reverse
+
+from servo import defaults
+from servo.models.common import Location
+
+
+class Queue(models.Model):
+
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ title = models.CharField(
+ max_length=255,
+ default=_('New Queue'),
+ verbose_name=_('Title')
+ )
+
+ keywords = models.TextField(
+ default='',
+ blank=True,
+ help_text=_('Orders with devices matching these keywords will be automatically assigned to this queue')
+ )
+
+ locations = models.ManyToManyField(
+ Location,
+ verbose_name=_('locations'),
+ help_text=_("Pick the locations you want this queue to appear in.")
+ )
+
+ description = models.TextField(
+ blank=True,
+ verbose_name=_('description')
+ )
+
+ PRIO_LOW = 0
+ PRIO_NORMAL = 1
+ PRIO_HIGH = 2
+
+ PRIORITIES = (
+ (PRIO_HIGH, _("High")),
+ (PRIO_NORMAL, _("Normal")),
+ (PRIO_LOW, _("Low"))
+ )
+
+ priority = models.IntegerField(
+ default=PRIO_NORMAL,
+ choices=PRIORITIES,
+ verbose_name=_("priority")
+ )
+
+ status_created = models.ForeignKey(
+ 'QueueStatus',
+ null=True,
+ blank=True,
+ related_name='+',
+ verbose_name=_(u'Order Created'),
+ help_text=_("Order has ben placed to a queue")
+ )
+
+ status_assigned = models.ForeignKey(
+ 'QueueStatus',
+ null=True,
+ blank=True,
+ related_name='+',
+ verbose_name=_(u'Order Assigned'),
+ help_text=_("Order has ben assigned to a user")
+ )
+
+ status_products_ordered = models.ForeignKey(
+ 'QueueStatus',
+ null=True,
+ blank=True,
+ related_name='+',
+ verbose_name=_("Products Ordered"),
+ help_text=_("Purchase Order for this Service Order has been submitted")
+ )
+ status_products_received = models.ForeignKey(
+ 'QueueStatus',
+ null=True,
+ blank=True,
+ related_name='+',
+ verbose_name=_("Products Received"),
+ help_text=_("Products have been received")
+ )
+ status_repair_completed = models.ForeignKey(
+ 'QueueStatus',
+ null=True,
+ blank=True,
+ related_name='+',
+ verbose_name=_("Repair Completed"),
+ help_text=_("GSX repair completed")
+ )
+
+ status_dispatched = models.ForeignKey(
+ 'QueueStatus',
+ null=True,
+ blank=True,
+ related_name='+',
+ verbose_name=_("Order Dispatched")
+ )
+
+ status_closed = models.ForeignKey(
+ 'QueueStatus',
+ null=True,
+ blank=True,
+ related_name='+',
+ verbose_name=_("Order Closed")
+ )
+
+ gsx_soldto = models.CharField(
+ blank=True,
+ default='',
+ max_length=10,
+ verbose_name=_("Sold-To"),
+ help_text=_("GSX queries of an order in this queue will be made using this Sold-To")
+ )
+
+ order_template = models.FileField(
+ null=True,
+ blank=True,
+ upload_to="templates",
+ verbose_name=_("order template"),
+ help_text=_("HTML template for Service Order/Work Confirmation")
+ )
+ quote_template = models.FileField(
+ null=True,
+ blank=True,
+ upload_to="templates",
+ verbose_name=_("quote template"),
+ help_text=_("HTML template for cost estimate")
+ )
+ receipt_template = models.FileField(
+ null=True,
+ blank=True,
+ upload_to="templates",
+ verbose_name=_("receipt template"),
+ help_text=_("HTML template for Sales Order Receipt")
+ )
+ dispatch_template = models.FileField(
+ null=True,
+ blank=True,
+ upload_to="templates",
+ verbose_name=_("dispatch template"),
+ help_text=_("HTML template for dispatched order")
+ )
+
+ objects = CurrentSiteManager()
+
+ def get_admin_url(self):
+ return reverse('admin-edit_queue', args=[self.pk])
+
+ def get_order_count(self, max_state=2):
+ count = self.order_set.filter(state__lt=max_state).count()
+ return count if count > 0 else ''
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ ordering = ['title']
+ app_label = "servo"
+ verbose_name = _("Queue")
+ verbose_name_plural = _("Queues")
+ unique_together = ('title', 'site',)
+
+
+class Status(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ FACTORS = (
+ (60, _('Minutes')),
+ (3600, _('Hours')),
+ (86400, _('Days')),
+ (604800, _('Weeks')),
+ (2419200, _('Months')),
+ )
+
+ title = models.CharField(
+ max_length=255,
+ default=_(u'New Status'),
+ verbose_name=_(u'name')
+ )
+ description = models.TextField(
+ null=True,
+ blank=True,
+ verbose_name=_(u'description')
+ )
+ limit_green = models.IntegerField(
+ default=1,
+ verbose_name=_(u'green limit')
+ )
+ limit_yellow = models.IntegerField(
+ default=15,
+ verbose_name=_(u'yellow limit')
+ )
+ limit_factor = models.IntegerField(
+ choices=FACTORS,
+ default=FACTORS[0],
+ verbose_name=_(u'time unit')
+ )
+ queue = models.ManyToManyField(
+ Queue,
+ editable=False,
+ through='QueueStatus'
+ )
+
+ def is_enabled(self, queue):
+ return self in queue.queuestatus_set.all()
+
+ def get_admin_url(self):
+ return reverse('admin-edit_status', args=[self.pk])
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ app_label = 'servo'
+ ordering = ('title',)
+ verbose_name = _('Status')
+ verbose_name_plural = _('Statuses')
+ unique_together = ('title', 'site',)
+
+
+class QueueStatus(models.Model):
+ """
+ A status bound to a queue.
+ This allows us to set time limits for each status per indiviudal queue
+ """
+ queue = models.ForeignKey(Queue)
+ status = models.ForeignKey(Status)
+
+ limit_green = models.IntegerField(default=1, verbose_name=_(u'green limit'))
+ limit_yellow = models.IntegerField(default=15, verbose_name=_(u'yellow limit'))
+ limit_factor = models.IntegerField(
+ choices=Status().FACTORS,
+ verbose_name=_(u'time unit'),
+ default=Status().FACTORS[0][0]
+ )
+
+ def get_green_limit(self):
+ """
+ Gets the green time limit for this QS
+ """
+ return timezone.now() + timedelta(seconds=self.limit_green*self.limit_factor)
+
+ def get_yellow_limit(self):
+ return timezone.now() + timedelta(seconds=self.limit_yellow*self.limit_factor)
+
+ def __unicode__(self):
+ return self.status.title
+
+ class Meta:
+ app_label = 'servo'
+ # A status should only be defined once per queue
+ unique_together = ('queue', 'status',)
diff --git a/servo/models/repair.py b/servo/models/repair.py
new file mode 100644
index 0000000..c45db2f
--- /dev/null
+++ b/servo/models/repair.py
@@ -0,0 +1,641 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+import gsxws
+import os.path
+
+from django.db import models
+from django.conf import settings
+from django.utils import timezone
+from django.core.urlresolvers import reverse
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+from django.core.validators import MaxLengthValidator
+from django.contrib.sites.managers import CurrentSiteManager
+
+from servo import defaults
+from servo.models.common import GsxAccount
+from servo.models import Queue, Order, Device, Product
+from servo.models.order import ServiceOrderItem
+from servo.models.parts import ServicePart
+from servo.models.purchases import PurchaseOrder, PurchaseOrderItem
+
+
+class Checklist(models.Model):
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+
+ title = models.CharField(
+ max_length=255,
+ unique=True,
+ verbose_name=_("title"),
+ default=_('New Checklist')
+ )
+ queues = models.ManyToManyField(
+ Queue,
+ blank=True,
+ verbose_name=_("queue")
+ )
+
+ enabled = models.BooleanField(default=True, verbose_name=_("Enabled"))
+ objects = CurrentSiteManager()
+
+ def get_admin_url(self):
+ return reverse('admin-edit_checklist', args=[self.pk])
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ app_label = "servo"
+ ordering = ("title",)
+ verbose_name = _('Checklist')
+ verbose_name_plural = _('Checklists')
+
+
+class ChecklistItem(models.Model):
+ checklist = models.ForeignKey(Checklist)
+ title = models.CharField(max_length=255, verbose_name=_("Task"))
+ description = models.TextField(
+ blank=True,
+ default='',
+ verbose_name=_('Description')
+ )
+ """
+ reported = models.BooleanField(
+ default=True,
+ verbose_name=_("Reported"),
+ help_text=_('Report this result to the customer')
+ )
+ """
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ app_label = "servo"
+
+
+class ChecklistItemValue(models.Model):
+ order = models.ForeignKey(Order)
+ item = models.ForeignKey(ChecklistItem)
+
+ checked_at = models.DateTimeField(auto_now_add=True)
+ checked_by = models.ForeignKey(settings.AUTH_USER_MODEL)
+
+ class Meta:
+ app_label = "servo"
+
+
+class Repair(models.Model):
+ """
+ Proxies service order data between our internal
+ service orders and GSX repairs
+ """
+ order = models.ForeignKey(Order, editable=False, on_delete=models.PROTECT)
+ device = models.ForeignKey(Device, editable=False, on_delete=models.PROTECT)
+ parts = models.ManyToManyField(ServiceOrderItem, through=ServicePart)
+
+ created_at = models.DateTimeField(auto_now_add=True, editable=False)
+ created_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ editable=False,
+ on_delete=models.PROTECT,
+ related_name="created_repairs"
+ )
+
+ tech_id = models.CharField(
+ default='',
+ blank=True,
+ max_length=15,
+ verbose_name=_(u'Technician')
+ )
+ unit_received_at = models.DateTimeField(
+ default=timezone.now,
+ verbose_name=_(u'Unit Received')
+ )
+ submitted_at = models.DateTimeField(null=True, editable=False)
+ completed_at = models.DateTimeField(null=True, editable=False)
+ completed_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ editable=False,
+ on_delete=models.PROTECT,
+ related_name="completed_repairs"
+ )
+ request_review = models.BooleanField(
+ default=False,
+ help_text=_("Repair should be reviewed by Apple before confirmation")
+ )
+ confirmation = models.CharField(max_length=10, default='', editable=False)
+ reference = models.CharField(
+ blank=True,
+ default='',
+ max_length=16,
+ verbose_name=_("Reference")
+ )
+
+ symptom = models.TextField()
+ diagnosis = models.TextField()
+ notes = models.TextField(
+ blank=True,
+ default='',
+ validators=[MaxLengthValidator(800)],
+ help_text=_("Notes are mandatory when requesting review.")
+ )
+ status = models.CharField(default='', editable=False, max_length=128)
+ attachment = models.FileField(
+ upload_to='repairs',
+ null=True,
+ blank=True,
+ help_text=_('Choose files to be sent with the repair creation request')
+ )
+ repair_number = models.CharField(default='', max_length=12, editable=False)
+ mark_complete = models.BooleanField(
+ blank=True,
+ default=False,
+ verbose_name=_("mark complete"),
+ help_text=_("Requires replacement serial number")
+ )
+ replacement_sn = models.CharField(
+ blank=True,
+ default='',
+ max_length=18,
+ verbose_name=_("New serial number"),
+ help_text=_("Serial Number of replacement part")
+ )
+ # the account through which this repair was submitted
+ gsx_account = models.ForeignKey(
+ GsxAccount,
+ editable=False,
+ on_delete=models.PROTECT
+ )
+
+ repair_type = models.CharField(
+ max_length=2,
+ default="CA",
+ editable=False,
+ choices=gsxws.REPAIR_TYPES
+ )
+
+ component_data = models.TextField(default='', editable=False)
+ consumer_law = models.NullBooleanField(
+ default=None,
+ help_text=_('Unit is eligible for consumer law coverage')
+ )
+
+ @property
+ def has_cl_parts(self):
+ """
+ Returns true if this repair contains Consumer Law parts
+ """
+ return self.servicepart_set.filter(coverage_status='VW').exists()
+
+ @property
+ def can_mark_complete(self):
+ """
+ Returns true if this repair can be marked complete after submitting
+ """
+ parts = self.servicepart_set.all()
+ if len(parts) > 1: return False
+ replacements = [p for p in parts if p.is_replacement()]
+ return len(replacements) == 1
+
+ @classmethod
+ def create_from_gsx(cls, order, confirmation):
+ """
+ Creates a new Repair for order with confirmation number
+ """
+ try:
+ repair = cls.objects.get(confirmation=confirmation)
+ msg = {'repair': repair.confirmation, 'order': repair.order}
+ raise ValueError(_('Repair %(repair)s already exists for order %(order)s') % msg)
+ except cls.DoesNotExist:
+ pass
+
+ repair = cls(order=order)
+ repair.confirmation=confirmation
+ repair.save()
+ repair.update_details()
+ repair.update_status()
+ return repair
+
+ def create_purchase_order(self):
+ # Create local purchase order
+ po = PurchaseOrder(supplier="Apple", created_by=self.created_by)
+ po.location = self.created_by.get_location()
+ po.reference = self.reference
+ po.sales_order = self.order
+ po.save()
+ return po
+
+ def warranty_status(self):
+ """
+ Gets warranty status for this device and these parts
+ """
+ self.connect_gsx(self.created_by)
+ product = gsxws.Product(self.device.sn)
+ parts = [(p.code, p.comptia_code,) for p in self.order.get_parts()]
+ return product.warranty(parts, self.get_received_date())
+
+ def is_open(self):
+ return self.completed_at is None
+
+ def get_products(self):
+ """
+ Returns the Service Order Items in this Repair
+ """
+ return [x.order_item for x in self.servicepart_set.all()]
+
+ def get_number(self, user=None):
+ return self.confirmation or _("New GSX Repair")
+
+ def set_parts(self, parts):
+ ServicePart.objects.filter(repair=self).delete()
+ for p in parts:
+ part = ServicePart.from_soi(self, p)
+ part.save()
+
+ def add_part(self, order_item, user):
+ """
+ Adds this Order Item as a part to this GSX repair
+ """
+ self.connect_gsx(user)
+ gsx_rep = self.get_gsx_repair()
+
+ part = ServicePart.from_soi(self, order_item)
+ order_line = part.get_repair_order_line()
+
+ gsx_rep.update({'orderLines': [order_line]})
+ part.order(user)
+
+ return part
+
+ def add_gsx_part(self, part):
+ """
+ Adds a part that has been added manually in GSX web UI
+ """
+ # part has been added to the order, but not the GSX repair
+ try:
+ oi = self.order.products.get(code=part.partNumber)
+ except ServiceOrderItem.DoesNotExist:
+ new_part = ServicePart(part_number=part.partNumber)
+ try:
+ p = Product.objects.get(code=part.partNumber)
+ except Product.DoesNotExist:
+ p = Product.from_gsx(new_part.lookup())
+ p.save()
+
+ oi = self.order.add_product(p, 1, self.created_by)
+
+ oi.comptia_code = part.comptiaCode or ''
+ oi.comptia_modifier = part.comptiaModifier or ''
+ oi.save()
+
+ sp = ServicePart.from_soi(self, oi)
+ sp.set_part_details(part)
+
+ sp.order(self.created_by)
+ sp.save()
+
+ def submit(self, customer_data):
+ """
+ Creates a new GSX repair and all the documentation that goes along with it
+ """
+ if len(self.parts.all()) < 1:
+ raise ValueError(_("Please add some parts to the repair"))
+
+ if not self.order.queue:
+ raise ValueError(_("Order has not been assigned to a queue"))
+
+
+ repair_data = self.to_gsx()
+
+ if self.repair_type == "CA":
+ gsx_repair = gsxws.CarryInRepair(**repair_data)
+ if self.repair_type == "ON":
+ gsx_repair = gsxws.IndirectOnsiteRepair(**repair_data)
+
+ customer_data['regionCode'] = self.gsx_account.region
+ gsx_repair.customerAddress = gsxws.Customer(**customer_data)
+
+ if self.component_data:
+ ccd = []
+ cd = json.loads(self.component_data)
+ for k, v in cd.items():
+ ccd.append(gsxws.ComponentCheck(component=k, serialNumber=v))
+
+ gsx_repair.componentCheckDetails = ccd
+
+ parts = [p.get_repair_order_line() for p in self.servicepart_set.all()]
+ gsx_repair.orderLines = parts
+
+ # Submit the GSX repair request
+ result = gsx_repair.create()
+
+ po = self.create_purchase_order()
+
+ for p in self.servicepart_set.all():
+ p.purchase_order = po
+ p.created_by = self.created_by
+ p.save()
+
+ poi = PurchaseOrderItem.from_soi(po, p.order_item, self.created_by)
+ poi.save()
+
+ confirmation = result.confirmationNumber
+ self.confirmation = confirmation
+ self.submitted_at = timezone.now()
+
+ po.confirmation = confirmation
+ po.submit(self.created_by)
+
+ self.save()
+
+ msg = _(u"GSX repair %s created") % confirmation
+ self.order.notify("gsx_repair_created", msg, self.created_by)
+
+ if repair_data.get("markCompleteFlag") is True:
+ self.close(self.created_by)
+
+ def get_gsx_repair(self):
+ return gsxws.CarryInRepair(self.confirmation)
+
+ def get_unit_received(self):
+ """
+ Returns (as a tuple) the GSX-compatible date and time of
+ when this unit was received
+ """
+ import locale
+ langs = gsxws.get_format('en_XXX')
+ ts = self.unit_received_at
+ loc = locale.getlocale()
+ # reset locale to get correct AM/PM value
+ locale.setlocale(locale.LC_TIME, None)
+ result = ts.strftime(langs['df']), ts.strftime(langs['tf'])
+ locale.setlocale(locale.LC_TIME, loc)
+ return result
+
+ def get_received_date(self):
+ return self.get_unit_received()[0]
+
+ def to_gsx(self):
+ """
+ Returns this Repair as a GSX-compatible dict
+ """
+ data = {'serialNumber': self.device.sn}
+ data['notes'] = self.notes
+ data['symptom'] = self.symptom
+ data['poNumber'] = self.reference
+ data['diagnosis'] = self.diagnosis
+ data['shipTo'] = self.gsx_account.ship_to
+ # checkIfOutOfWarrantyCoverage
+ if self.tech_id:
+ data['diagnosedByTechId'] = self.tech_id
+
+ ts = self.get_unit_received()
+ data['unitReceivedDate'] = ts[0]
+ data['unitReceivedTime'] = ts[1]
+
+ if self.attachment:
+ data['fileData'] = self.attachment
+ data['fileName'] = os.path.basename(self.attachment.name)
+
+ if self.mark_complete:
+ data['markCompleteFlag'] = self.mark_complete
+ data['replacementSerialNumber'] = self.replacement_sn
+
+ data['requestReviewByApple'] = self.request_review
+
+ if self.consumer_law is not None:
+ data['consumerLawEligible'] = self.consumer_law
+
+ return data
+
+ def has_serialized_parts(self):
+ """
+ Checks if this Repair has any serialized parts
+ """
+ count = self.parts.filter(servicepart__order_item__product__is_serialized=True).count()
+ return count > 0
+
+ def check_components(self):
+ """
+ Runs GSX component check for this repair's parts
+ """
+ l = gsxws.Lookup(serialNumber=self.device.sn)
+ l.repairStrategy = self.repair_type
+ l.shipTo = self.gsx_account.ship_to
+ parts = []
+
+ for i in self.servicepart_set.all():
+ part = gsxws.ServicePart(i.part_number)
+ part.symptomCode = i.comptia_code
+ parts.append(part)
+
+ try:
+ r = l.component_check(parts)
+ except gsxws.GsxError, e:
+ if e.code == "COMP.LKP.004":
+ return # Symptom Code not required for Replacement and Other category parts.
+ raise e
+
+ if r.componentDetails is None:
+ return
+
+ if len(self.component_data) < 1:
+ d = {}
+ for i in r.componentDetails:
+ f = i.componentCode
+ d[f] = i.componentDescription
+
+ self.component_data = json.dumps(d)
+
+ return self.component_data
+
+ def connect_gsx(self, user=None):
+ """
+ Initialize the GSX session with the right credentials.
+ User can also be different from the one who initially created the repair.
+ """
+ account = user or self.created_by
+ self.gsx_account.connect(account)
+
+ def set_status(self, new_status, user):
+ """
+ Sets the current status of this repair to new_status
+ and notifies the corresponding Service Order
+ """
+ if not new_status == self.status:
+ self.status = new_status
+ self.save()
+ self.order.notify("repair_status_changed", self.status, user)
+
+ def update_status(self, user):
+ repair = self.get_gsx_repair()
+ status = repair.status().repairStatus
+ self.set_status(status, user)
+
+ return self.status
+
+ def get_details(self):
+ repair = self.get_gsx_repair()
+ details = repair.details()
+
+ if isinstance(details.partsInfo, dict):
+ details.partsInfo = [details.partsInfo]
+
+ self.update_details(details)
+ return details
+
+ def get_return_label(self, part):
+ self.get_details()
+ part = self.servicepart_set.get(pk=part)
+ return part.get_return_label()
+
+ def update_details(self, details):
+ """
+ Updates what local info we have about this particular GSX repair
+ """
+ part_list = list(self.servicepart_set.all().order_by('id'))
+
+ for i, p in enumerate(details.partsInfo):
+ try:
+ part = part_list[i]
+ part.set_part_details(p)
+ part.save()
+ except IndexError: # part added in GSX web ui...
+ self.add_gsx_part(p)
+ except AttributeError: # some missing attribute in set_part_details()
+ pass
+
+ def get_replacement_sn(self):
+ """
+ Try to guess replacement part's SN
+ """
+ oi = self.order.serviceorderitem_set.filter(
+ product__is_serialized=True,
+ product__part_type="REPLACEMENT"
+ )
+
+ try:
+ return oi[0].sn
+ except IndexError:
+ pass
+
+ def complete(self, user):
+ self.completed_at = timezone.now()
+ self.completed_by = user
+ self.save()
+
+ queue = self.order.queue
+ if queue.status_repair_completed:
+ status = queue.status_repair_completed
+ self.order.set_status(status, user)
+
+ def close(self, user):
+ """
+ Marks this GSX repair as complete
+ """
+ self.connect_gsx(user)
+ repair = self.get_gsx_repair()
+
+ try:
+ # Update part serial numbers
+ [part.update_sn() for part in self.servicepart_set.all()]
+ repair.mark_complete()
+ except gsxws.GsxError as e:
+ """
+ Valid GSX errors are:
+ 'ACT.BIN.01': Repair # provided is not valid. Please enter a valid repair #.
+ 'RPR.LKP.01': No Repair found matching search criteria.
+ 'RPR.LKP.010': No Repair found matching the search criteria.
+ 'RPR.COM.030': Cannot mark repair as complete for Unit $1. Repair is not open.
+ 'RPR.COM.036': Repair for Unit $1 is already marked as complete.
+ 'RPR.COM.019': This repair cannot be updated.
+ 'RPR.LKP.16': This Repair Cannot be Updated.Repair is not Open.
+ 'RPR.COM.136': Repair $1 cannot be marked complete as the Warranty
+ Claims Certification Form status is either Declined or Hold.
+ 'ENT.UPL.022': 'Confirmation # $1 does not exist.'
+ """
+ errorlist = (
+ 'ACT.BIN.01',
+ 'RPR.LKP.01',
+ 'RPR.LKP.010',
+ 'RPR.COM.030',
+ 'RPR.COM.036',
+ 'RPR.COM.019',
+ 'RPR.LKP.16',
+ 'RPR.COM.136',
+ 'ENT.UPL.022',
+ )
+
+ if e.code not in errorlist:
+ raise e
+
+ status = repair.status()
+ self.set_status(status.repairStatus, user)
+
+ self.complete(user)
+
+ def duplicate(self, user):
+ """
+ Makes a copy of this GSX Repair
+ """
+ new_rep = Repair(order=self.order, created_by=user, device=self.device)
+ new_rep.repair_type = self.repair_type
+ new_rep.tech_id = self.tech_id
+ new_rep.symptom = self.symptom
+ new_rep.diagnosis = self.diagnosis
+ new_rep.notes = self.notes
+ new_rep.reference = self.reference
+ new_rep.request_review = self.request_review
+ new_rep.mark_complete = self.mark_complete
+ new_rep.unit_received_at = self.unit_received_at
+ new_rep.attachment = self.attachment
+ new_rep.gsx_account = self.gsx_account
+
+ new_rep.save()
+ new_rep.set_parts(self.order.get_parts())
+
+ return new_rep
+
+ def get_absolute_url(self):
+ if self.submitted_at is None:
+ return reverse('repairs-edit_repair', args=[self.order.pk, self.pk])
+ return reverse('repairs-view_repair', args=[self.order.pk, self.pk])
+
+ def __unicode__(self):
+ if self.pk is not None:
+ return _("Repair %d") % self.pk
+
+ class Meta:
+ app_label = "servo"
+ get_latest_by = "created_at"
diff --git a/servo/models/rules.py b/servo/models/rules.py
new file mode 100644
index 0000000..db54ec6
--- /dev/null
+++ b/servo/models/rules.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.db import models
+from django.core.cache import cache
+
+from django.dispatch import receiver
+from django.db.models.signals import post_save
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from servo.models import Event, Queue
+
+
+class ServoModel(models.Model):
+ class Meta:
+ abstract = True
+ app_label = "servo"
+
+
+class Rule(ServoModel):
+ description = models.CharField(max_length=128, default=_('New Rule'))
+ MATCH_CHOICES = (
+ ('ANY', _('Any')),
+ ('ALL', _('All')),
+ )
+ match = models.CharField(
+ max_length=3,
+ default='ANY',
+ choices=MATCH_CHOICES
+ )
+
+ def as_dict(self):
+ d = {'description': self.description}
+ d['match'] = self.match
+ return d
+
+ def serialize(self):
+ """
+ Returns this rule as a JSON-string
+ """
+ import json
+ d = self.as_dict()
+ d['conditions'] = []
+ d['actions'] = []
+
+ for i in self.condition_set.all():
+ d['conditions'].append(i.as_dict())
+
+ for i in self.action_set.all():
+ d['actions'].append(i.as_dict())
+
+ return json.dumps(d)
+
+
+ def get_name(self):
+ return self.description
+
+ def get_admin_url(self):
+ return reverse('rules-edit_rule', args=[self.pk])
+
+ def apply(self, event):
+ order = event.content_object
+ for a in self.action_set.all():
+ a.apply(order, event)
+
+ def __unicode__(self):
+ return self.description
+
+
+class Condition(ServoModel):
+ rule = models.ForeignKey(Rule)
+
+ EVENT_MAP = {
+ 'device_added': 'DEVICE',
+ }
+
+ KEY_CHOICES = (
+ ('QUEUE', _('Queue')),
+ ('STATUS', _('Status')),
+ ('DEVICE', _('Device name')),
+ ('CUSTOMER_NAME', _('Customer name')),
+ )
+
+ key = models.CharField(max_length=16, choices=KEY_CHOICES)
+ OPERATOR_CHOICES = (
+ ('^%s$', _('Equals')),
+ ('%s', _('Contains')),
+ ('%d < %d', _('Less than')),
+ ('%d > %d', _('Greater than')),
+ )
+ operator = models.CharField(
+ max_length=4,
+ default='^%s$',
+ choices=OPERATOR_CHOICES
+ )
+ value = models.TextField(default='')
+
+ def as_dict(self):
+ d = {'key': self.key}
+ d['operator'] = self.operator
+ d['value'] = self.value
+ return d
+
+ def __unicode__(self):
+ return '%s %s %s' % (self.key, self.operator, self.value)
+
+
+class Action(ServoModel):
+ rule = models.ForeignKey(Rule)
+
+ KEY_CHOICES = (
+ ('SEND_SMS', _('Send SMS')),
+ ('SEND_EMAIL', _('Send email')),
+ ('ADD_TAG', _('Add Tag')),
+ ('SET_PRIO', _('Set Priority')),
+ ('SET_QUEUE', _('Set Queue')),
+ ('SET_USER', _('Assign to')),
+ )
+
+ key = models.CharField(
+ max_length=32,
+ default='SEND_EMAIL',
+ choices=KEY_CHOICES
+ )
+ value = models.TextField(default='')
+
+ def as_dict(self):
+ d = {'key': self.key}
+ d['value'] = self.value
+ return d
+
+ def apply(self, order, event):
+ if self.key == 'SET_QUEUE':
+ order.set_queue(self.value, event.triggered_by)
+
+ if self.key == 'SET_USER':
+ order.set_user(self.value, event.triggered_by)
+
+ def __unicode__(self):
+ return '%s %s' % (self.key, self.value)
+
+
+@receiver(post_save, sender=Event)
+def process_event(sender, instance, created, **kwargs):
+ try:
+ condition = Condition.EVENT_MAP[instance.action]
+ print condition
+ for r in Rule.objects.filter(condition__key=condition):
+ print 'APPLYING %s' % condition
+ r.apply(instance)
+ except KeyError:
+ return # no mapping for this event
diff --git a/servo/models/shipments.py b/servo/models/shipments.py
new file mode 100644
index 0000000..2330f0d
--- /dev/null
+++ b/servo/models/shipments.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+
+from django.db import models
+
+from django.conf import settings
+from django.utils import timezone
+from django.core.files import File
+from django.utils.translation import ugettext_lazy as _
+
+from servo.models import Location
+
+
+class Shipment(models.Model):
+ """
+ Bulk returns
+ """
+ RETURN_DOA = 1 # Dead On Arrival
+ RETURN_GPR = 2 # Good Part Return
+ RETURN_CTS = 3 # Convert to stock
+
+ location = models.ForeignKey(Location, editable=False)
+
+ ship_to = models.CharField(
+ default='',
+ max_length=10,
+ editable=False,
+ )
+
+ return_id = models.CharField(
+ null=True,
+ unique=True,
+ max_length=10,
+ editable=False,
+ help_text="The return ID returned by GSX"
+ )
+
+ tracking_id = models.CharField(
+ blank=True,
+ default='',
+ max_length=30,
+ verbose_name=_('Tracking ID'),
+ help_text="Carrier's tracking ID"
+ )
+
+ tracking_url = models.URLField(
+ null=True,
+ editable=False,
+ help_text="The tracking URL returned by GSX"
+ )
+
+ packing_list = models.FileField(
+ null=True,
+ editable=False,
+ upload_to='returns',
+ help_text="The PDF returned by GSX"
+ )
+
+ carrier = models.CharField(
+ blank=True,
+ default='',
+ max_length=18,
+ choices=gsxws.CARRIERS,
+ verbose_name=_('carrier')
+ )
+
+ created_at = models.DateTimeField(auto_now=True, editable=False)
+
+ created_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ editable=False,
+ on_delete=models.SET_NULL,
+ related_name="created_shipments"
+ )
+
+ dispatched_at = models.DateTimeField(null=True, editable=False)
+
+ dispatched_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ editable=False,
+ related_name='dispatched_shipments'
+ )
+
+ width = models.PositiveIntegerField(
+ null=True,
+ blank=True,
+ verbose_name=_('width')
+ )
+
+ height = models.PositiveIntegerField(
+ null=True,
+ blank=True,
+ verbose_name=_('height')
+ )
+
+ length = models.PositiveIntegerField(
+ null=True,
+ blank=True,
+ verbose_name=_('length')
+ )
+
+ weight = models.PositiveIntegerField(
+ null=True,
+ blank=True,
+ verbose_name=_('weight')
+ )
+
+ @classmethod
+ def get_current(cls, user, location, ship_to):
+ try:
+ shipment = cls.objects.get(dispatched_at=None,
+ location=location,
+ ship_to=ship_to)
+ except cls.DoesNotExist:
+ shipment = cls.objects.create(created_by=user,
+ location=location,
+ ship_to=ship_to)
+
+ return shipment
+
+ def toggle_part(self, part):
+ part.registered_for_return = not part.registered_for_return
+ part.save()
+
+ if part.registered_for_return:
+ self.servicepart_set.add(part)
+ else:
+ self.servicepart_set.remove(part)
+
+ def verify(self):
+ """
+ Verifies this shipment with GSX
+ """
+ pass
+
+ def register_bulk_return(self, user):
+ """
+ Registers bulk return with GSX
+ """
+ parts = [] # Array of outbound parts in GSX format
+
+ gsx_act = self.location.gsx_accounts.get(ship_to=self.ship_to)
+ gsx_act.connect(user)
+
+ for p in self.servicepart_set.all().order_by('box_number'):
+ parts.append(p.to_gsx())
+
+ ret = gsxws.Return(shipToCode=self.ship_to)
+
+ ret.notes = ""
+ ret.width = self.width
+ ret.length = self.length
+ ret.height = self.height
+ ret.carrierCode = self.carrier
+ ret.trackingNumber = self.tracking_id
+ ret.estimatedTotalWeight = self.weight
+
+ result = ret.register_parts(parts)
+ ret.bulkReturnOrder = parts
+ self.dispatched_by = user
+ self.dispatched_at = timezone.now()
+ self.return_id = result.bulkReturnId
+ self.tracking_url = result.trackingURL
+
+ self.save()
+
+ filename = "bulk_return_%s.pdf" % self.return_id
+ self.packing_list.save(filename, File(open(result.packingList)))
+
+ def get_absolute_url(self):
+ return "/products/shipments/returns/%d/" % self.pk
+
+ def save(self, *args, **kwargs):
+ if not self.pk:
+ self.location = self.created_by.location
+
+ super(Shipment, self).save(*args, **kwargs)
+
+ def __unicode__(self):
+ return u'Shipment #%s from %s' % (self.pk, self.location.title)
+
+ class Meta:
+ app_label = "servo"
+ get_latest_by = 'id'
+ ordering = ('-dispatched_at',)
diff --git a/servo/stats/__init__.py b/servo/stats/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/stats/__init__.py
diff --git a/servo/stats/forms.py b/servo/stats/forms.py
new file mode 100644
index 0000000..d1ec2aa
--- /dev/null
+++ b/servo/stats/forms.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django import forms
+from datetime import timedelta
+from django.utils import timezone
+from servo.forms import DatepickerInput
+from django.utils.translation import ugettext as _
+
+from servo.models import Location, Group, Status, Tag, Queue
+
+default_timescale = 'WEEK'
+default_end_date = timezone.now()
+default_start_date = default_end_date - timedelta(days=30)
+
+
+class BasicStatsForm(forms.Form):
+ timescale = forms.ChoiceField(
+ label=_('Time Scale'),
+ choices=(
+ ('DAY', _('Day')),
+ ('WEEK', _('Week')),
+ ('MONTH', _('Month'))
+ )
+ )
+ start_date = forms.DateField(
+ label=_('Start date'),
+ widget=DatepickerInput(attrs={'class': "input-small"})
+ )
+ end_date = forms.DateField(
+ label=_('End date'),
+ widget=DatepickerInput(attrs={'class': "input-small"})
+ )
+
+
+class OrderStatsForm(BasicStatsForm):
+ location = forms.ModelChoiceField(
+ required=False,
+ label=_('Location'),
+ queryset=Location.objects.all()
+ )
+
+
+class TechieStatsForm(OrderStatsForm):
+ group = forms.ModelChoiceField(
+ required=False,
+ label=_('Group'),
+ queryset=Group.objects.all()
+ )
+
+
+class StatusStatsForm(OrderStatsForm):
+ status = forms.ModelChoiceField(
+ label=_('Status'),
+ queryset=Status.objects.all()
+ )
+
+
+class InvoiceStatsForm(BasicStatsForm):
+ pass
+
+
+class NewStatsForm(forms.Form):
+ location = forms.ModelMultipleChoiceField(
+ queryset=Location.objects.all()
+ )
+ queue = forms.ModelMultipleChoiceField(
+ queryset=Queue.objects.all(),
+ widget=forms.SelectMultiple
+ )
+ label = forms.ModelMultipleChoiceField(
+ queryset=Tag.objects.filter(type="order"),
+ required=False
+ )
+ status = forms.ModelMultipleChoiceField(
+ queryset=Status.objects.all(),
+ required=False
+ )
+ start_date = forms.DateField(
+ widget=DatepickerInput(attrs={'class': "input-small"}),
+ initial=default_start_date
+ )
+ end_date = forms.DateField(
+ widget=DatepickerInput(attrs={'class': "input-small"}),
+ initial=default_end_date
+ )
diff --git a/servo/stats/queries.py b/servo/stats/queries.py
new file mode 100644
index 0000000..1b892fe
--- /dev/null
+++ b/servo/stats/queries.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import decimal
+from django.db import connection
+
+
+class StatsManager:
+ def __init__(self):
+ self.cursor = connection.cursor()
+
+ def _result(self, args):
+ result = []
+ self.cursor.execute(self.sql, args)
+ for k, v in self.cursor.fetchall():
+ if isinstance(v, decimal.Decimal):
+ v = float(v)
+ result.append((k, v,))
+
+ return result
+
+ def cases_per_tech(self, location, queues, labels, start, end):
+ users = User.object.filter(location=location)
+
+
+ def statuses_per_location(self, timescale, location, status, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, se.triggered_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order so, servo_event se
+ WHERE (se.triggered_at, se.triggered_at) OVERLAPS (%s, %s)
+ AND se.action = 'set_status'
+ AND se.object_id = so.id
+ AND so.location_id = %s
+ AND se.description = %s
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, start, end, location, status])
+
+ def statuses_per_user(self, timescale, user, status, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, se.triggered_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order so, servo_event se
+ WHERE (se.triggered_at, se.triggered_at) OVERLAPS (%s, %s)
+ AND se.action = 'set_status'
+ AND se.object_id = so.id
+ AND so.user_id = %s
+ AND se.description = %s
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, start, end, user, status])
+
+ def sales_invoices(self, timescale, queue, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, so.created_at))*1000 as p,
+ SUM(total_gross) AS v
+ FROM servo_invoice si, servo_order so
+ WHERE (si.created_at, si.created_at) OVERLAPS (%s, %s)
+ AND si.order_id = so.id
+ AND so.queue_id = %s
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, start, end, queue])
+
+ def sales_purchases(self, timescale, queue, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, po.created_at))*1000 as p,
+ SUM(total) AS v
+ FROM servo_purchaseorder po, servo_order so
+ WHERE (po.created_at, po.created_at) OVERLAPS (%s, %s)
+ AND po.sales_order_id = so.id
+ AND so.queue_id = %s
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, start, end, queue])
+
+ def sales_parts_per_labtier(self, start, end):
+ self.sql = """SELECT labour_tier, count(*)
+ FROM servo_product p, servo_servicepart sp, servo_serviceorderitem soi
+ WHERE soi.product_id = p.id
+ AND sp.order_item_id = soi.id
+ AND (soi.created_at, soi.created_at) OVERLAPS (%s, %s)
+ AND char_length(labour_tier) = 4
+ GROUP BY labour_tier
+ ORDER BY labour_tier"""
+
+ return self._result([start, end])
+
+ def order_runrate(self, timescale, location, user, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, started_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order
+ WHERE user_id = %s
+ AND location_id = %s
+ AND (started_at, started_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, user, location, start, end])
+
+ def turnaround_per_location(self, timescale, location, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ EXTRACT(HOUR FROM AVG(closed_at - created_at)) as v
+ FROM servo_order
+ WHERE closed_at IS NOT NULL
+ AND location_id = %s
+ AND queue_id IS NOT NULL
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, start, end])
+
+ def runrate_per_location(self, timescale, location, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order
+ WHERE location_id = %s
+ AND closed_at IS NOT NULL
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, start, end])
+
+ def distribution_per_location(self, start, end):
+ result = []
+ self.sql = """SELECT l.title, COUNT(*)
+ FROM servo_order o LEFT OUTER JOIN servo_location l on (o.location_id = l.id)
+ WHERE (o.created_at, o.created_at) OVERLAPS (%s, %s)
+ GROUP BY l.title"""
+ self.cursor.execute(self.sql, [start, end])
+
+ for k, v in self.cursor.fetchall():
+ result.append({'label': k, 'data': v})
+
+ return result
+
+ def orders_created_by(self, timescale, location, user, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order
+ WHERE location_id = %s
+ AND created_by_id = %s
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, user, start, end])
+
+ def orders_created_at(self, timescale, location, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order
+ WHERE location_id = %s
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, start, end])
+
+ def orders_closed_at(self, timescale, location, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order
+ WHERE location_id = %s
+ AND (closed_at, closed_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, start, end])
+
+ def orders_closed_in(self, timescale, location, queue, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order
+ WHERE location_id = %s
+ AND queue_id = %s
+ AND (closed_at, closed_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, queue, start, end])
+
+ def order_count(self, timescale, location, queue, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ COUNT(*) AS v
+ FROM servo_order
+ WHERE location_id = %s
+ AND queue_id = %s
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, queue, start, end])
+
+ def order_turnaround(self, timescale, location, queue, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ EXTRACT(HOUR FROM AVG(closed_at - created_at)) as v
+ FROM servo_order
+ WHERE closed_at IS NOT NULL
+ AND location_id = %s
+ AND queue_id = %s
+ AND queue_id IS NOT NULL
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, queue, start, end])
+
+ def order_turnaround(self, timescale, location, queue, start, end):
+ self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p,
+ EXTRACT(HOUR FROM AVG(closed_at - created_at)) as v
+ FROM servo_order
+ WHERE closed_at IS NOT NULL
+ AND location_id = %s
+ AND queue_id = %s
+ AND queue_id IS NOT NULL
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY p
+ ORDER BY p ASC"""
+
+ return self._result([timescale, location, queue, start, end])
diff --git a/servo/tasks.py b/servo/tasks.py
new file mode 100644
index 0000000..c302cca
--- /dev/null
+++ b/servo/tasks.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from __future__ import absolute_import
+
+from celery import shared_task
+
+from django.core.cache import cache
+
+from servo.models import Order, Note
+
+
+def get_rules():
+ import json
+ fh = open("local_rules.json", "r")
+ rules = json.load(fh)
+ cache.set('rules', rules)
+ return rules
+
+
+@shared_task
+def apply_rules(event):
+
+ rules = cache.get('rules', get_rules())
+
+ for r in rules:
+ if (r['event'] == event.action) and (r['match'] == event.description):
+
+ if r['action'] == "set_queue":
+ order = event.content_object
+ order.set_queue(r['data'], event.triggered_by)
+
+ if r['action'] == "send_sms":
+ number = 0
+ order = event.content_object
+
+ try:
+ number = order.customer.get_standard_phone()
+ except Exception as e:
+ continue
+
+ note = Note(order=order, created_by=event.triggered_by)
+
+ note.body = r['data']
+ note.render_body({'order': order})
+ note.save()
+
+ return note.send_sms(number, event.triggered_by)
+
+
+@shared_task
+def batch_process(user, data):
+ """
+ /orders/batch
+ """
+ processed = 0
+ orders = data['orders'].strip().split("\r\n")
+
+ for o in orders:
+ try:
+ order = Order.objects.get(code=o)
+ except Exception as e:
+ continue
+
+ if data['status'] and order.queue:
+ status = order.queue.queuestatus_set.get(status_id=data['status'])
+ order.set_status(status, user)
+
+ if data['queue']:
+ order.set_queue(data['queue'], user)
+
+ if len(data['sms']) > 0:
+ try:
+ number = order.customer.get_standard_phone()
+ note = Note(order=order, created_by=user, body=data['sms'])
+ note.render_body({'order': order})
+ note.save()
+
+ try:
+ note.send_sms(number, user)
+ except Exception as e:
+ note.delete()
+ print("Failed to send SMS to: %s" % number)
+
+ except AttributeError as e: # customer has no phone number
+ continue
+
+ if len(data['email']) > 0:
+ note = Note(order=order, created_by=user, body=data['email'])
+ note.sender = user.email
+
+ try:
+ note.recipient = order.customer.email
+ note.render_subject({'note': note})
+ note.render_body({'order': order})
+ note.save()
+ note.send_mail(user)
+ except Exception as e:
+ # customer has no email address or some other error...
+ pass
+
+ if len(data['note']) > 0:
+ note = Note(order=order, created_by=user, body=data['note'])
+ note.render_body({'order': order})
+ note.save()
+
+ processed += 1
+
+ return '%d/%d orders processed' % (processed, len(orders))
+
diff --git a/servo/templates/about.html b/servo/templates/about.html
new file mode 100755
index 0000000..c8a6521
--- /dev/null
+++ b/servo/templates/about.html
@@ -0,0 +1,25 @@
+{% load staticfiles %}
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Servo Credits</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link href="{{ STATIC_URL }}js/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+ <script src="{{ STATIC_URL }}js/jquery.min.js"></script>
+ <script src="{{ STATIC_URL }}js/bootstrap/js/bootstrap.min.js"></script>
+ </head>
+ <body>
+ <div class="container">
+ <div class="text-center">
+ <img src="{{ STATIC_URL }}images/servo-logo-large.gif" alt=""/></div>
+ <p class="text-center">
+ <h5>Creator and Developer</h5>
+ Filipp Lepalaaan
+ <h5>Special Thanks</h5>
+ Janne, Rami, Joonas and Vesa
+ </p>
+ </div>
+ <script src="http://code.jquery.com/jquery.js"></script>
+ <script src="js/bootstrap.min.js"></script>
+ </body>
+</html>
diff --git a/servo/templates/accounts/calendar_form.html b/servo/templates/accounts/calendar_form.html
new file mode 100755
index 0000000..4094d01
--- /dev/null
+++ b/servo/templates/accounts/calendar_form.html
@@ -0,0 +1,12 @@
+{% extends "snippets/modal.html" %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block body %}
+ <form method="post" action="{{ action }}" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ </form>
+{% endblock body %}
diff --git a/servo/templates/accounts/calendars.html b/servo/templates/accounts/calendars.html
new file mode 100755
index 0000000..c10276c
--- /dev/null
+++ b/servo/templates/accounts/calendars.html
@@ -0,0 +1,55 @@
+{% extends "accounts/orders.html" %}
+{% load i18n %}
+{% load servo_tags %}
+
+{% block toolbar %}
+<div class="btn-group">
+ <a href="{% url 'calendars-create' username=request.user.username %}" data-modal="#modal" class="btn"><i class="icon-plus"></i> {% trans "New Calendar" %}</a>
+{% if calendar %}
+ <a href="{% url 'calendars.event.edit' username=request.user.username cal_pk=calendar.pk %}" class="btn">{% trans "New Event" %}</a>
+{% endif %}
+</div>
+
+<div class="btn-group">
+{% if calendar %}
+ <a href="print/" class="btn window"><i class="icon-print"></i> {% trans "Print" %}</a>
+ <a href="{% url 'calendars-edit' request.user.username calendar.pk view %}" data-modal="#modal" class="btn"><i class="icon-pencil"></i> {% trans "Edit" %}</a>
+ {% if perms.servo.delete_calendar %}
+ <a href="{{ calendar.get_absolute_url }}delete/" data-modal="#modal" class="btn"><i class="icon-trash"></i> {% trans "Delete" %}</a>
+ {% else %}
+ <a href="" class="btn disabled">{% trans "Delete" %}</a>
+ {% endif %}
+{% endif %}
+</div>
+
+<div class="btn-group">
+ <a href="download/" class="btn {% if not calendar %}disabled{% endif %}"><i class="icon-download"></i> {% trans "Download" %}</a>
+</div>
+
+{% endblock toolbar %}
+
+{% block second_row %}
+
+<div class="row-fluid">
+ <div class="span3">
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Calendars" %}</li>
+ {% for c in calendars %}
+ <li class="{% active request c.pk '/' %}">
+ <a href="{% url 'calendars.view' pk=c.pk username=request.user.username view='week' %}">{{ c.title }}<span class="badge pull-right">{{ c.get_unfinished_count }}</span></a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+
+ <div class="span9">
+ {% block detail_view %}
+ <h1 class="muted text-center">{% trans "No calendar selected" %}</h1>
+ {% endblock detail_view %}
+ </div>
+</div>
+{% endblock second_row %}
+
+{% block crumbs %}
+ <li class="active"><a href="{% url 'calendars-list' username=request.user.username %}">{% trans "Calendars" %}</a></li>
+{% endblock crumbs %}
diff --git a/servo/templates/accounts/delete_calendar.html b/servo/templates/accounts/delete_calendar.html
new file mode 100755
index 0000000..bae2e16
--- /dev/null
+++ b/servo/templates/accounts/delete_calendar.html
@@ -0,0 +1,17 @@
+{% extends "snippets/modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block body %}
+ {% trans "This will also delete all events in this calendar" %}
+{% endblock body %}
+
+{% block footer %}
+ <form method="post" action="{{ action }}">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/accounts/delete_calendar_event.html b/servo/templates/accounts/delete_calendar_event.html
new file mode 100755
index 0000000..58fbef3
--- /dev/null
+++ b/servo/templates/accounts/delete_calendar_event.html
@@ -0,0 +1,13 @@
+{% extends "snippets/modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/accounts/edit_calendar_event.html b/servo/templates/accounts/edit_calendar_event.html
new file mode 100755
index 0000000..7316791
--- /dev/null
+++ b/servo/templates/accounts/edit_calendar_event.html
@@ -0,0 +1,14 @@
+{% extends "accounts/calendars.html" %}
+
+{% block detail_view %}
+ <form action="" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ {% include "form_field_snippet.html" with field=form.started_at %}
+ <div class="input-append">
+ {{ form.finished_at }}
+ <button class="btn" type="button" id="set_dt"><i class="icon-time"></i></button>
+ </div>
+ {% include "form_field_snippet.html" with field=form.notes %}
+ {% include "form_buttons.html" %}
+ </form>
+{% endblock detail_view %}
diff --git a/servo/templates/accounts/login.html b/servo/templates/accounts/login.html
new file mode 100755
index 0000000..80ba716
--- /dev/null
+++ b/servo/templates/accounts/login.html
@@ -0,0 +1,22 @@
+{% extends "login.html" %}
+{% load i18n %}
+
+{% block content %}
+<img src="{{ STATIC_URL }}images/logo_servoapp.png" class="servo-logo" alt=""/>
+<form action="{% url "accounts-login" %}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <div class="input-prepend">
+ <span class="add-on"><i class="icon-user"></i></span>
+ {{ form.username }}
+ </div>
+ <div class="input-prepend">
+ <span class="add-on"><i class="icon-lock"></i></span>
+ {{ form.password }}
+ </div>
+ <br/>
+ <div class="pull-right">
+ <a class="btn btn" href="{% url 'checkin-index' %}">{% trans "Check-in" %}</a>
+ <button class="btn btn-primary" type="submit">{% trans "Login" %}</button>
+ </div>
+</form>
+{% endblock content %}
diff --git a/servo/templates/accounts/logout.html b/servo/templates/accounts/logout.html
new file mode 100755
index 0000000..1b64d45
--- /dev/null
+++ b/servo/templates/accounts/logout.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Logging out?" %}
+{% endblock header %}
+
+{% block body %}
+{% trans "This will terminate your Servo session." %}
+{% endblock body %}
+
+{% block footer %}
+<form action="{% url 'accounts-logout' %}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Log out" %}</button>
+</form>
+{% endblock footer %}
diff --git a/servo/templates/accounts/orders.html b/servo/templates/accounts/orders.html
new file mode 100755
index 0000000..8420ad8
--- /dev/null
+++ b/servo/templates/accounts/orders.html
@@ -0,0 +1,59 @@
+{% extends "default.html" %}
+{% load i18n %}
+{% load servo_tags %}
+
+{% block toolbar %}
+{% if perms.servo.add_order %}
+ <a href="{% url 'orders-create' %}" class="btn"><i class="icon-plus"></i> {% trans "Create Order" %}</a>
+{% endif %}
+{% endblock toolbar %}
+
+{% block content %}
+<div class="page-header clearfix">
+ <div class="pull-left">
+ <h2><i class="glyphicons-icon home"></i> {{ title }}<br/><small style="margin-left:60px">{{ subtitle }}</small></h2>
+ </div>
+ <form class="form-search pull-right">
+ <div class="input-append">
+ <input type="text" class="search-query filter" placeholder="{% trans "Filter results" %}"/>
+ <button type="button" class="btn" data-toggle="collapse" data-target="#collapsable"><i class="icon-search"></i></button>
+ </div>
+ </form>
+</div>
+
+<div class="row-fluid">
+ <div class="span12">
+ {% include "accounts/tabs.html" %}
+ </div>
+</div>
+
+{% block second_row %}
+
+<div id="collapsable" class="collapse out">
+ <div class="well clearfix">
+ <form action="" method="get" class="form-inline">
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <p class="clearfix"><hr/></p>
+ <div class="pull-right">
+ <a class="btn" href="{% url 'accounts-list_orders' username=request.user.username %}?user=state=1&amp;user={{ request.user.pk }}"><i class="icon-refresh"></i> {% trans "Reset" %}</a>
+ <button class="btn btn-primary" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </form>
+ </div>
+</div>
+{% block second_column %}
+ {% include "orders/list.html" %}
+{% endblock second_column %}
+</div>
+{% endblock second_row %}
+
+{% endblock content %}
+
+{% block crumbs %}
+<li class="active">{% trans "Orders" %}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/accounts/print_calendar.html b/servo/templates/accounts/print_calendar.html
new file mode 100755
index 0000000..64edbe3
--- /dev/null
+++ b/servo/templates/accounts/print_calendar.html
@@ -0,0 +1,82 @@
+{% extends "default_print.html" %}
+{% load servo_tags %}
+{% load mptt_tags %}
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+<div class="row">
+ <div class="span4">
+ {% if location.logo %}
+ <img src="{% get_media_prefix %}{{ location.logo }}" alt="logo" title="logo" class="media-object pull-left span2" style="margin:10px"/>
+ {% endif %}
+ </div>
+ <div class="span4">
+ {{ location.title }}<br/>
+ {{ location.address }}<br/>
+ {{ location.zip_code }}, {{ location.city }}<br/>
+ {{ location.notes }}
+ </div>
+ <div class="span4">
+ {% block location_info %}
+ {% endblock location_info %}
+ {% trans "Email Address" %}: {{ location.email }}<br/>
+ {% trans "Phone" %}: {{ location.phone }}<br/><br/>
+ </div>
+</div>
+<div class="row" style="margin-top:2em;margin-bottom:1em">
+ <div class="span6">
+ {% trans "Employee" %}: {{ calendar.user.get_full_name }}<br/>
+ {% trans "Date" %}: {% now "SHORT_DATE_FORMAT" %}
+ </div>
+</div>
+<div class="row">
+ <div class="span12 text-center"><h3>{{ title }}</h3></div>
+</div>
+<div class="row">
+ <div class="span12">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Date" %}</th>
+ <th>{% trans "Started At" %}</th>
+ <th>{% trans "Finished At" %}</th>
+ <th>{% trans "Duration" %}</th>
+ <th>{% trans "Notes" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for i in events %}
+ <tr>
+ <td>{% ifchanged i.started_at|date %}{{ i.started_at|date:"SHORT_DATE_FORMAT" }}{% endifchanged %}</td>
+ <td>{{ i.started_at|time:"TIME_FORMAT" }}</td>
+ <td>{{ i.finished_at|time:"TIME_FORMAT"|default:"-" }}</td>
+ <td>{{ i.get_duration }}</td>
+ <td>{{ i.notes|default:'' }}</td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="6" class="muted empty">{% trans "No events found" %}</td></tr>
+ {% endfor %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td><strong>{% trans "Total" %}</strong></td>
+ <td colspan="4"><strong>{{ subtitle }}</strong></td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+</div>
+<br/>
+<br/>
+<div class="row">
+ <div class="span6">
+ <hr/>
+ {% trans "Manager" %}
+ </div>
+ <div class="span6">
+ <hr/>
+ {% trans "Employee" %}
+ </div>
+</div>
+{% endblock content %}
diff --git a/servo/templates/accounts/register.html b/servo/templates/accounts/register.html
new file mode 100755
index 0000000..e1410ff
--- /dev/null
+++ b/servo/templates/accounts/register.html
@@ -0,0 +1,14 @@
+{% extends "login.html" %}
+{% load i18n %}
+
+{% block content %}
+ <form method="post" action="">
+ <h2>{% trans "Register" %}</h2>
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="pull-right">
+ <a href="/login/" class="btn">{% trans "Back" %}</a>
+ <button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
+ </div>
+ </form>
+{% endblock content %}
diff --git a/servo/templates/accounts/settings.html b/servo/templates/accounts/settings.html
new file mode 100755
index 0000000..a48ab19
--- /dev/null
+++ b/servo/templates/accounts/settings.html
@@ -0,0 +1,46 @@
+{% extends "accounts/orders.html" %}
+{% load i18n %}
+
+{% block first_column %}
+{% endblock first_column %}
+
+{% block second_row %}
+<form action="" method="post" accept-charset="utf-8" class="form-horizontal" enctype="multipart/form-data">{% csrf_token %}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Profile" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Locale" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "Password" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "GSX" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.location %}
+ {% include "form_field_snippet.html" with field=form.queues %}
+ {% include "form_field_snippet.html" with field=form.should_notify %}
+ {% include "form_field_snippet.html" with field=form.notify_by_email %}
+ {% include "form_field_snippet.html" with field=form.autoprint %}
+ {% include "form_field_snippet.html" with field=form.photo %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "form_field_snippet.html" with field=form.locale %}
+ {% include "form_field_snippet.html" with field=form.region %}
+ {% include "form_field_snippet.html" with field=form.timezone %}
+ </div>
+ <div class="tab-pane" id="tab3">
+ {% include "form_field_snippet.html" with field=form.password1 %}
+ {% include "form_field_snippet.html" with field=form.password2 %}
+ </div>
+ <div class="tab-pane" id="tab4">
+ {% include "form_field_snippet.html" with field=form.tech_id %}
+ {% include "form_field_snippet.html" with field=form.gsx_userid %}
+ {% include "form_field_snippet.html" with field=form.gsx_password %}
+ {% include "form_field_snippet.html" with field=form.gsx_poprefix %}
+ </div>
+ </div>
+ {% include "form_buttons.html" %}
+</form>
+{% endblock second_row %}
+
+{% block crumbs %}
+ <li class="active">{% trans "Settings" %}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/accounts/stats.html b/servo/templates/accounts/stats.html
new file mode 100755
index 0000000..d56e1a5
--- /dev/null
+++ b/servo/templates/accounts/stats.html
@@ -0,0 +1,42 @@
+{% extends "accounts/orders.html" %}
+{% load i18n %}
+
+{% block second_row %}
+<div class="row-fluid">
+ <div class="span12 well">
+ {% block filter_block %}
+ <form action="#" method="post" class="form-inline" id="stats-form">
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <div class="control-group">
+ <label class="control-label">&nbsp;</label>
+ <div class="controls">
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </div>
+ </form>
+ {% endblock filter_block %}
+ </div>
+</div>
+
+<div class="resizable">
+ <h2>{% trans "Runrate" %}</h2>
+ <p>{% trans "Shows you how many you've been assigned to during the selected period, averaged over the time scale." %}</p>
+ <div class="plot" data-source="/stats/data/runrate/personal/"></div>
+ <div class="legend-container"></div>
+</div>
+{% endblock second_row %}
+
+{% block media %}
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.min.js"></script>
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.time.min.js"></script>
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.pie.min.js"></script>
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.resize.min.js"></script>
+<script src="{{ STATIC_URL }}js/jquery.flot.tooltip.min.js"></script>
+<script src="{{ STATIC_URL }}js/stats.js" type="text/javascript"></script>
+{% endblock media %}
diff --git a/servo/templates/accounts/tabs.html b/servo/templates/accounts/tabs.html
new file mode 100755
index 0000000..4444ab8
--- /dev/null
+++ b/servo/templates/accounts/tabs.html
@@ -0,0 +1,22 @@
+{% load i18n %}
+{% load servo_tags %}
+
+<ul class="nav nav-tabs">
+{% with request.user.username as username %}
+ <li class="{% active request "orders" %}">
+ <a href="{% url 'accounts-list_orders' username=username %}">{% trans "Orders" %}</a>
+ </li>
+ <li class="{% active request "updates" %}">
+ <a href="{% url 'accounts-updates' username=username %}">{% trans "Updates" %}</a>
+ </li>
+ <li class="{% active request "calendars" %}">
+ <a href="{% url 'calendars-list' username=username %}">{% trans "Calendars" %}</a>
+ </li>
+ <li class="{% active request "stats" %}">
+ <a href="{% url 'accounts-stats' username=username %}">{% trans "Statistics" %}</a>
+ </li>
+ <li class="{% active request "settings" %}">
+ <a href="{% url 'accounts-settings' username=username %}">{% trans "Settings" %}</a>
+ </li>
+{% endwith %}
+</ul>
diff --git a/servo/templates/accounts/updates.html b/servo/templates/accounts/updates.html
new file mode 100644
index 0000000..121aea9
--- /dev/null
+++ b/servo/templates/accounts/updates.html
@@ -0,0 +1,65 @@
+{% extends "default.html" %}
+{% load humanize %}
+{% load i18n %}
+
+{% block toolbar %}
+{% if perms.servo.add_order %}
+ <a href="{% url 'accounts-clear_notifications' request.user.username %}?t={% now "Y/m/d/H/i" %}" class="btn"><i class="icon-ok"></i> {% trans "Clear all" %}</a>
+{% endif %}
+{% endblock toolbar %}
+
+{% block content %}
+<div class="page-header clearfix">
+ <div class="pull-left">
+ <h2><i class="glyphicons-icon home"></i> {{ title }}<br/><small style="margin-left:60px">{{ subtitle }}</small></h2>
+ </div>
+</div>
+<div class="row-fluid">
+ <div class="span12">
+ {% include "accounts/tabs.html" %}
+ </div>
+</div>
+{% block second_row %}
+<div class="row-fluid">
+ <div class="span3">
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Updates" %}</li>
+ <li {% if kind == 'note_added' %}class="active"{% endif %}><a href="?kind=note_added">{% trans "Messages" %}</a></li>
+ <li {% if kind == 'set_status' %}class="active"{% endif %}><a href="?kind=set_status">{% trans "Statuses" %}</a></li>
+ </ul>
+ </div>
+ <div class="span9">
+ <table class="table">
+ <thead>
+ <tr>
+ <th style="width:25px"></th>
+ <th>{% trans "Event" %}</th>
+ <th style="width:100px">{% trans "Order" %}</th>
+ <th style="width:150px"></th>
+ <th style="width:24px"></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for event in events %}
+ <tr>
+ <td><img src="{{ STATIC_URL }}images/{{ event.get_icon }}.png" alt="{{ event.description }}" class="icon"/></td>
+ <td>{{ event.description }}</td>
+ <td><a href="{{ event.content_object.get_absolute_url }}">{{ event.content_object }}</a></td>
+ <td style="text-align:right">{{ event.triggered_by }}<br/>
+ <small class="muted">{{ event.triggered_at|naturaltime }}</small></td>
+ <td><a class="btn {% if event.handled_at %}disabled{% endif %}" href="{% url 'events-ack_event' event.pk %}?return=0"><i class="icon-ok"></i></a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% include "pagination.html" with items=events %}
+ </div>
+</div>
+
+{% endblock second_row %}
+
+{% endblock content %}
+
+{% block crumbs %}
+<li class="active">{% trans "Updates" %}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/accounts/view_calendar.html b/servo/templates/accounts/view_calendar.html
new file mode 100755
index 0000000..2b063e2
--- /dev/null
+++ b/servo/templates/accounts/view_calendar.html
@@ -0,0 +1,66 @@
+{% extends "accounts/calendars.html" %}
+{% load i18n %}
+{% load servo_tags %}
+
+{% block detail_view %}
+
+<div class="text-center">
+{% block view_block %}
+<div class="btn-group">
+ <a class="btn {% active request 'day' %}" href="{{ calendar.get_absolute_url }}day/">{% trans "Day" %}</a>
+ <a class="btn {% active request 'week' %}" href="{{ calendar.get_absolute_url }}week/">{% trans "Week" %}</a>
+ <a class="btn {% active request 'month' %}" href="{{ calendar.get_absolute_url }}month/">{% trans "Month" %}</a>
+</div>
+{% endblock view_block %}
+
+{% block browse_block %}
+<div class="btn-group pull-right">
+ <a href="{% url 'calendars-view_calendar' username=request.user.username pk=calendar.pk view=view start_date=previous|date:'Y-m-d' %}" class="btn"><i class="icon-chevron-left"></i></a>
+ <a href="{{ base_url }}" class="btn">{% trans "Today" %}</a>
+ <a href="{% url 'calendars-view_calendar' username=request.user.username pk=calendar.pk view=view start_date=next|date:'Y-m-d' %}" class="btn"><i class="icon-chevron-right"></i></a>
+</div>
+{% endblock browse_block %}
+</div>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Date" %}</th>
+ <th>{% trans "Started At" %}</th>
+ <th>{% trans "Finished At" %}</th>
+ <th>{% trans "Duration" %}</th>
+ <th>{% trans "Notes" %}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for i in events %}
+ <tr>
+ <td>{% ifchanged i.started_at|date %}{{ i.started_at|date:"SHORT_DATE_FORMAT" }}{% endifchanged %}</td>
+ <td>{{ i.started_at|time:"TIME_FORMAT" }}</td>
+ <td>{{ i.finished_at|time:"TIME_FORMAT"|default:"-" }}</td>
+ <td>{{ i.get_duration }}</td>
+ <td>{{ i.notes|default:'' }}</td>
+ <td style="text-align:right">
+ <div class="btn-group">
+ {% if i.finished_at %}
+ <a href="" class="btn disabled"><i class="icon-time"></i></a>
+ {% else %}
+ <a href="{{ calendar.get_absolute_url }}events/{{ i.pk }}/finish/" class="btn"><i class="icon-time"></i></a>
+ {% endif %}
+ <a href="{{ calendar.get_absolute_url }}events/{{ i.pk }}/edit/" class="btn btn-warning"><i class="icon-pencil icon-white"></i></a>
+ <a href="{{ calendar.get_absolute_url }}events/{{ i.pk }}/delete/" class="btn btn-danger" data-modal="#modal"><i class="icon-trash icon-white"></i></a>
+ </div>
+ </td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="6" class="muted empty">{% trans "No events found" %}</td></tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock detail_view %}
+
+{% block crumbs %}
+ <li><a href="{% url 'calendars-list' username=request.user.username %}">{% trans "Calendars" %}</a> <span class="divider">/</span></li>
+ <li class="active">{{ calendar.title }}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/admin/backups.html b/servo/templates/admin/backups.html
new file mode 100644
index 0000000..1670ea5
--- /dev/null
+++ b/servo/templates/admin/backups.html
@@ -0,0 +1,17 @@
+{% extends "admin/index.html" %}
+{% load i18n %}
+
+{% block second_column %}
+ <h2>{% trans "Backups" %}</h2>
+ <table class="table">
+ <tbody>
+ {% for b in backups %}
+ <tr>
+ <td>{{ b.filename }}</td>
+ <td>{{ b.filesize|filesizeformat }}</td>
+ <td><a href="?dl={{ b.filename }}" class="btn btn-default"><i class="icon-download"></i></a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+{% endblock second_column %}
diff --git a/servo/templates/admin/checklist/form.html b/servo/templates/admin/checklist/form.html
new file mode 100644
index 0000000..3b44576
--- /dev/null
+++ b/servo/templates/admin/checklist/form.html
@@ -0,0 +1,31 @@
+{% extends "admin/checklist/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<form method="post" action="" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ {{ formset.management_form }}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Tasks" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_snippet.html" %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% for f in formset %}
+ {% include "form_snippet.html" with form=f %}
+ {% endfor %}
+ </div>
+ </div>
+ <div class="form-actions">
+ {% if checklist.pk %}
+ <a class="btn btn-danger" data-modal="#modal" href="{% url 'admin-delete_checklist' checklist.pk %}">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/checklist/index.html b/servo/templates/admin/checklist/index.html
new file mode 100644
index 0000000..f4a2820
--- /dev/null
+++ b/servo/templates/admin/checklist/index.html
@@ -0,0 +1,6 @@
+{% extends "generic/admin_list.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a class="btn btn-default navbar-btn" href="{% url 'admin-create_checklist' %}"><i class="icon-plus"></i> {% trans "New Checklist" %}</a>
+{% endblock toolbar %}
diff --git a/servo/templates/admin/fields/form.html b/servo/templates/admin/fields/form.html
new file mode 100644
index 0000000..f377594
--- /dev/null
+++ b/servo/templates/admin/fields/form.html
@@ -0,0 +1,17 @@
+{% extends "admin/fields/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+ <form method="post" action="" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="form-actions">
+ {% if field.pk %}
+ <a class="btn btn-danger" data-modal="#modal" href="{% url 'admin-delete_field' field.pk %}">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+ </form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/fields/index.html b/servo/templates/admin/fields/index.html
new file mode 100644
index 0000000..e005c75
--- /dev/null
+++ b/servo/templates/admin/fields/index.html
@@ -0,0 +1,30 @@
+{% extends "generic/admin_list.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+<a href="{% url 'admin-create_field' type=type %}" class="btn btn-default navbar-btn"><i class="icon-plus"></i> {% trans "New Field" %}</a>
+{% endblock toolbar %}
+
+{% block second_column %}
+<div class="row-fluid">
+ <ul class="nav nav-tabs">
+ {% for k, v in types %}
+ <li class="{% active request k %}"><a href="{% url 'admin-fields' type=k %}">{{ v }}</a></li>
+ {% endfor %}
+ </ul>
+</div>
+<div class="row-fluid">
+ <div class="span3">
+ <ul class="nav nav-pills nav-stacked">
+ {% for o in object_list %}
+ <li class="{% active_url request o.get_admin_url %}"><a href="{{ o.get_admin_url }}">{{ o.title|truncatechars:25 }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="span9">
+ {% block third_column %}
+ {% endblock third_column %}
+ </div>
+</div>
+{% endblock second_column %}
diff --git a/servo/templates/admin/fields/remove.html b/servo/templates/admin/fields/remove.html
new file mode 100644
index 0000000..04c2c93
--- /dev/null
+++ b/servo/templates/admin/fields/remove.html
@@ -0,0 +1,8 @@
+{% load i18n %}
+<form action="/fields/remove/" method="post" accept-charset="utf-8" rel="fields">
+ {% csrf_token %}
+ <input type="hidden" name="id" value="{{ id }}" />
+ <img src="/static/images/alert_48.png" alt="alert" class="alert" />
+ <h2>{% trans "Are you sure you want to delete this field?" %}</h2>
+ <p>{% trans "This action cannot be undone." %}</p>
+</form>
diff --git a/servo/templates/admin/gsx/form.html b/servo/templates/admin/gsx/form.html
new file mode 100755
index 0000000..b154d5d
--- /dev/null
+++ b/servo/templates/admin/gsx/form.html
@@ -0,0 +1,20 @@
+{% extends "admin/gsx/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#" data-toggle="tab">{% trans "General" %}</a></li>
+</ul>
+<form method="post" action="" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="form-actions">
+ {% if act.pk %}
+ <a class="btn btn-danger" data-modal="#modal" href="{% url 'admin-delete_gsx_account' act.pk %}">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/gsx/index.html b/servo/templates/admin/gsx/index.html
new file mode 100755
index 0000000..f137e29
--- /dev/null
+++ b/servo/templates/admin/gsx/index.html
@@ -0,0 +1,6 @@
+{% extends "generic/admin_list.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'admin-edit_gsx_account' %}" class="btn btn-default navbar-btn"><i class="icon-plus"></i> {% trans "New Account" %}</a>
+{% endblock toolbar %}
diff --git a/servo/templates/admin/gsx/remove.html b/servo/templates/admin/gsx/remove.html
new file mode 100755
index 0000000..184e154
--- /dev/null
+++ b/servo/templates/admin/gsx/remove.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Remove GSX account" %}?
+{% endblock header %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/admin/index.html b/servo/templates/admin/index.html
new file mode 100644
index 0000000..e982e28
--- /dev/null
+++ b/servo/templates/admin/index.html
@@ -0,0 +1,50 @@
+{% extends "two_column_layout.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block first_column %}
+<ul class="nav nav-list">
+ <li class="nav-header">{% trans "System Settings" %}</li>
+ <li class="{% active request "settings" %}">
+ <a href="{% url 'admin-settings' %}"><i class="icon-cog"></i> {% trans "Settings" %}</a>
+ </li>
+ <li class="{% active request "gsx" %}">
+ <a href="{% url 'admin-list_gsx_accounts' %}"><i class="icon-globe"></i> {% trans "GSX Accounts" %}</a>
+ </li>
+ <li class="{% active request "locations" %}">
+ <a href="{% url 'admin-locations' %}"><i class="icon-map-marker"></i> {% trans "Locations" %}</a>
+ </li>
+ <li class="{% active request "statuses" %}">
+ <a href="{% url 'admin-statuses' %}"><i class="icon-tasks"></i> {% trans "Statuses" %}</a>
+ </li>
+ <li class="{% active request "queues" %}">
+ <a href="{% url 'admin-queues' %}"><i class="icon-inbox"></i> {% trans "Queues" %}</a>
+ </li>
+ <li class="{% active request "(users)|(groups)" %}">
+ <a href="{% url 'admin-list_users' %}"><i class="icon-user"></i> {% trans "Users & Groups" %}</a>
+ </li>
+ <li class="{% active request "fields" %}">
+ <a href="{% url 'admin-fields' type='customer' %}"><i class="icon-list-alt"></i> {% trans "Fields" %}</a>
+ </li>
+ <li class="{% active request "tags" %}">
+ <a href="{% url 'admin-tags' type='device' %}"><i class="icon-tags"></i> {% trans "Tags" %}</a>
+ </li>
+ <li class="{% active request "templates" %}">
+ <a href="{% url 'admin-list_templates' %}"><i class="icon-file"></i> {% trans "Templates" %}</a>
+ </li>
+ <li class="{% active request "checklists" %}">
+ <a href="{% url 'admin-checklists' %}"><i class="icon-cog"></i> {% trans "Checklists" %}</a>
+ </li>
+ <li class="{% active request "backups" %}">
+ <a href="{% url 'admin-backups' %}"><i class="icon-download"></i> {% trans "Backups" %}</a>
+ </li>
+</ul>
+{% endblock first_column %}
+
+{% block second_column %}
+
+{% endblock second_column %}
+
+{% block crumbs %}
+ <li class="active">{% trans "Systen Settings" %}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/admin/inventory/index.html b/servo/templates/admin/inventory/index.html
new file mode 100644
index 0000000..ef31079
--- /dev/null
+++ b/servo/templates/admin/inventory/index.html
@@ -0,0 +1,18 @@
+{% extends "admin/index.html" %}
+{% load i18n %}
+
+{% block second_column %}
+<form method="post" accept-charset="utf-8" action="" class="form-horizontal">
+ {% csrf_token %}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Stock" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Categories" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ </div>
+ <div class="tab-pane" id="tab2">
+ </div>
+ </div>
+</form>
+{% endblock second_column %}
diff --git a/servo/templates/admin/locations/form.html b/servo/templates/admin/locations/form.html
new file mode 100644
index 0000000..777c9ab
--- /dev/null
+++ b/servo/templates/admin/locations/form.html
@@ -0,0 +1,40 @@
+{% extends "admin/locations/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "GSX" %}</a></li>
+</ul>
+
+<form method="post" action="" accept-charset="utf-8" enctype="multipart/form-data" class="form-horizontal">
+ {% csrf_token %}
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.title %}
+ {% include "form_field_snippet.html" with field=form.email %}
+ {% include "form_field_snippet.html" with field=form.phone %}
+ {% include "form_field_snippet.html" with field=form.address %}
+ {% include "form_field_snippet.html" with field=form.zip_code %}
+ {% include "form_field_snippet.html" with field=form.city %}
+ {% include "form_field_snippet.html" with field=form.timezone %}
+ {% include "form_field_snippet.html" with field=form.notes %}
+ {% include "form_field_snippet.html" with field=form.logo %}
+ {% include "form_field_snippet.html" with field=form.enabled %}
+ </div>
+ <div class="tab-pane" id="tab3">
+ {% include "form_field_snippet.html" with field=form.gsx_shipto %}
+ {% include "form_field_snippet.html" with field=form.gsx_accounts %}
+ {% include "form_field_snippet.html" with field=form.gsx_tz %}
+ </div>
+ </div>
+ <div class="form-actions">
+ {% if location.pk %}
+ <a class="btn btn-danger" data-modal="#modal" href="{% url 'admin-delete_location' location.pk %}">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/locations/index.html b/servo/templates/admin/locations/index.html
new file mode 100644
index 0000000..92fe940
--- /dev/null
+++ b/servo/templates/admin/locations/index.html
@@ -0,0 +1,6 @@
+{% extends "generic/admin_list.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'admin-create_location' %}" class="btn btn-default navbar-btn"><i class="icon-plus"></i> {% trans "New Location" %}</a>
+{% endblock toolbar %}
diff --git a/servo/templates/admin/notifications/form.html b/servo/templates/admin/notifications/form.html
new file mode 100644
index 0000000..ce4a8f9
--- /dev/null
+++ b/servo/templates/admin/notifications/form.html
@@ -0,0 +1,5 @@
+{% extends "admin/index.html" %}
+
+{% block admin_content %}
+
+{% endblock admin_content %}
diff --git a/servo/templates/admin/notifications/index.html b/servo/templates/admin/notifications/index.html
new file mode 100644
index 0000000..1fd05a0
--- /dev/null
+++ b/servo/templates/admin/notifications/index.html
@@ -0,0 +1,39 @@
+{% extends "admin/index.html" %}
+{% load i18n %}
+
+{% block second_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#">{% trans "Reports" %}</a></li>
+</ul>
+
+<form method="post" action="">
+ {% csrf_token %}
+ <table class="table">
+ <thead>
+ <tr>
+ <th></th>
+ <th>{% trans "Daily" %}</th>
+ <th>{% trans "Weekly" %}</th>
+ <th>{% trans "Monthly" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>{% trans "Repairs aging beyond limits" %}</td>
+ <td><input type="checkbox"/></td>
+ <td><input type="checkbox"/></td>
+ <td><input type="checkbox"/></td>
+ </tr>
+ <tr>
+ <td>{% trans "Products below stocking limit" %}</td>
+ <td><input type="checkbox"/></td>
+ <td><input type="checkbox"/></td>
+ <td><input type="checkbox"/></td>
+ </tr>
+ </tbody>
+ </table>
+ <div class="form-actions">
+ <button type="submit" class="btn btn-primary pull-right">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock second_column %}
diff --git a/servo/templates/admin/queues/form.html b/servo/templates/admin/queues/form.html
new file mode 100644
index 0000000..119275a
--- /dev/null
+++ b/servo/templates/admin/queues/form.html
@@ -0,0 +1,81 @@
+{% extends "admin/queues/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<form method="post" action="" accept-charset="utf-8" class="form-horizontal" enctype="multipart/form-data">
+ {% csrf_token %}
+ {{ formset.management_form }}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Statuses" %}</a></li>
+ <li><a href="#tab5" data-toggle="tab">{% trans "Defaults" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "GSX" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "Templates" %}</a></li>
+ <li><a href="#tab6" data-toggle="tab">{% trans "Users" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.title %}
+ {% include "form_field_snippet.html" with field=form.priority %}
+ {% include "form_field_snippet.html" with field=form.description %}
+ <!--{% include "form_field_snippet.html" with field=form.keywords %}//-->
+ {% include "form_field_snippet.html" with field=form.locations %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ <table class="table table-condensed" id="table-queue-status">
+ <thead>
+ <tr>
+ <th>{% trans "Status" %}</th>
+ <th colspan="3">{% trans "Time limits" %}</th>
+ <th style="width:30px" class="text-center">{% trans "Delete" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for form in formset %}
+ <tr class="form">
+ {% for field in form %}
+ {% if field.is_hidden %}
+ {{ field }}
+ {% else %}
+ <td>{{ field }}</td>
+ {% endif %}
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <a class="btn" id="add_more"><i class="icon-plus"></i> {% trans "Add Status" %}</a>
+ </div>
+ <div class="tab-pane" id="tab5">
+ {% include "form_field_snippet.html" with field=form.status_created %}
+ {% include "form_field_snippet.html" with field=form.status_assigned %}
+ {% include "form_field_snippet.html" with field=form.status_products_ordered %}
+ {% include "form_field_snippet.html" with field=form.status_products_received %}
+ {% include "form_field_snippet.html" with field=form.status_repair_completed %}
+ {% include "form_field_snippet.html" with field=form.status_dispatched %}
+ {% include "form_field_snippet.html" with field=form.status_closed %}
+ </div>
+ <div class="tab-pane" id="tab3">
+ {% include "form_field_snippet.html" with field=form.gsx_soldto %}
+ </div>
+ <div class="tab-pane" id="tab4">
+ {% include "form_field_snippet.html" with field=form.order_template %}
+ {% include "form_field_snippet.html" with field=form.quote_template %}
+ {% include "form_field_snippet.html" with field=form.receipt_template %}
+ {% include "form_field_snippet.html" with field=form.dispatch_template %}
+ </div>
+ <div class="tab-pane" id="tab6">
+ {% include "form_field_snippet.html" with field=form.users %}
+ </div>
+ </div>
+ {% include "form_buttons.html" %}
+</form>
+{% endblock third_column %}
+
+{% block media %}
+<script type="text/javascript">
+ $('#add_more').click(function() {
+ cloneMore('tr.form:last', 'queuestatus_set', true);
+ });
+</script>
+{% endblock media %}
diff --git a/servo/templates/admin/queues/index.html b/servo/templates/admin/queues/index.html
new file mode 100644
index 0000000..d161dbb
--- /dev/null
+++ b/servo/templates/admin/queues/index.html
@@ -0,0 +1,6 @@
+{% extends "generic/admin_list.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'admin-create_queue' %}" class="btn btn-default navbar-btn"><i class="icon-plus"></i> {% trans "New Queue" %}</a>
+{% endblock toolbar %}
diff --git a/servo/templates/admin/queues/remove.html b/servo/templates/admin/queues/remove.html
new file mode 100644
index 0000000..125f89e
--- /dev/null
+++ b/servo/templates/admin/queues/remove.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with queue.title as title %}Delete queue "{{ title }}"?{% endblocktrans %}
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans "Orders in this queue will not be deleted." %}</p>
+{% endblock body %}
+
+{% block footer %}
+ <form action="{% url 'admin-delete_queue' queue.pk %}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/admin/settings.html b/servo/templates/admin/settings.html
new file mode 100644
index 0000000..15266b7
--- /dev/null
+++ b/servo/templates/admin/settings.html
@@ -0,0 +1,136 @@
+{% extends "admin/index.html" %}
+{% load i18n %}
+
+{% block second_column %}
+<form method="post" accept-charset="utf-8" action="" enctype="multipart/form-data" class="form-horizontal">
+ {% csrf_token %}
+<!--
+ {{ formset.management_form }}
+//-->
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Stock" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "Outgoing Mail" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "Incoming Mail" %}</a></li>
+ <li><a href="#tab5" data-toggle="tab">{% trans "Text Messages" %}</a></li>
+ <li><a href="#tab6" data-toggle="tab">{% trans "Check-in" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.company_name %}
+ {% include "form_field_snippet.html" with field=form.company_logo %}
+ {% include "form_field_snippet.html" with field=form.gsx_account %}
+ {% include "form_field_snippet.html" with field=form.terms_of_service %}
+ {% include "form_field_snippet.html" with field=form.autocomplete_repairs %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "form_field_snippet.html" with field=form.currency %}
+ {% include "form_field_snippet.html" with field=form.pct_margin %}
+ {% include "form_field_snippet.html" with field=form.pct_vat %}
+ {% include "form_field_snippet.html" with field=form.shipping_cost %}
+ {% include "form_field_snippet.html" with field=form.track_inventory %}
+ <div class="control-group">
+ <div class="controls">
+ <label class="checkbox">
+ <input name="update_prices" type="checkbox" value="1"/> {% trans "Update prices" %}
+ </label>
+ <span class="help-block">{% trans "Applies the new settings to existing products after saving" %}</span>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="tab3">
+ <fieldset>
+ {% include "form_field_snippet.html" with field=form.default_sender %}
+ {% include "form_field_snippet.html" with field=form.default_sender_custom %}
+ {% include "form_field_snippet.html" with field=form.default_subject %}
+ {% include "form_field_snippet.html" with field=form.smtp_host %}
+ {% include "form_field_snippet.html" with field=form.smtp_user %}
+ {% include "form_field_snippet.html" with field=form.smtp_password %}
+ {% include "form_field_snippet.html" with field=form.smtp_ssl %}
+ </fieldset>
+ <fieldset>
+ <legend>{% trans "Notifications" %}</legend>
+ {% include "form_field_snippet.html" with field=form.notify_address %}
+ {% include "form_field_snippet.html" with field=form.notify_location %}
+ </fieldset>
+ </div>
+ <div class="tab-pane" id="tab4">
+ {% include "form_field_snippet.html" with field=form.imap_act %}
+ {% include "form_field_snippet.html" with field=form.imap_host %}
+ {% include "form_field_snippet.html" with field=form.imap_user %}
+ {% include "form_field_snippet.html" with field=form.imap_password %}
+ {% include "form_field_snippet.html" with field=form.imap_ssl %}
+ </div>
+ <div class="tab-pane" id="tab5">
+ {% include "form_field_snippet.html" with field=form.sms_gateway %}
+ {% if config.sms_gateway == 'builtin' %}
+ <p class="text-muted text-center">{% trans "Sending SMS messages will be handled by Servo" %}</p>
+ {% endif %}
+ {% if config.sms_gateway == 'http' %}
+ {% include "form_field_snippet.html" with field=form.sms_http_url %}
+ {% include "form_field_snippet.html" with field=form.sms_http_user %}
+ {% include "form_field_snippet.html" with field=form.sms_http_password %}
+ {% endif %}
+ {% if config.sms_gateway == 'smtp' %}
+ {% include "form_field_snippet.html" with field=form.sms_smtp_address %}
+ {% endif %}
+ {% if config.sms_gateway == 'jazz' %}
+ {% include "form_field_snippet.html" with field=form.sms_http_user %}
+ {% include "form_field_snippet.html" with field=form.sms_http_password %}
+ {% include "form_field_snippet.html" with field=form.sms_http_sender %}
+ {% endif %}
+ {% if config.sms_gateway == 'hqsms' %}
+ {% include "form_field_snippet.html" with field=form.sms_http_user %}
+ {% include "form_field_snippet.html" with field=form.sms_http_password %}
+ {% include "form_field_snippet.html" with field=form.sms_http_sender %}
+ {% endif %}
+ </div>
+ <div class="tab-pane" id="tab6">
+ {% include "form_field_snippet.html" with field=form.checkin_user %}
+ {% include "form_field_snippet.html" with field=form.checkin_group %}
+ {% include "form_field_snippet.html" with field=form.checkin_queue %}
+ {% include "form_field_snippet.html" with field=form.checkin_checklist %}
+ {% include "form_field_snippet.html" with field=form.checkin_password %}
+ {% include "form_field_snippet.html" with field=form.checkin_require_password %}
+ {% include "form_field_snippet.html" with field=form.checkin_require_condition %}
+ {% include "form_field_snippet.html" with field=form.checkin_report_checklist %}
+ {% include "form_field_snippet.html" with field=form.checkin_timeline %}
+<!--
+ <fieldset>
+ <legend>{% trans "Shipping Methods" %}</legend>
+ {% for f in formset %}
+ <div class="form">
+ {% include "form_snippet.html" with form=f %}
+ </div>
+ {% endfor %}
+ </fieldset>
+ <a class="btn" id="add_more"><i class="icon-plus"></i> {% trans "Add Method" %}</a>
+//-->
+ </div>
+ </div>
+ <div class="form-actions">
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock second_column %}
+
+{% block media %}
+<script type="text/javascript">
+ $(function(){
+ $('#id_default_sender').change(function(){
+ if( $(this).val() == 'custom' ) {
+ $('#id_default_sender_custom').attr('disabled', false);
+ $('#id_default_sender_custom').focus();
+ } else {
+ $('#id_default_sender_custom').attr('disabled', true);
+ }
+ });
+ $('#id_default_sender').trigger('change');
+
+ $('#add_more').click(function() {
+ cloneMore('div.form:last', 'form', false);
+ });
+
+ });
+</script>
+{% endblock media %}
diff --git a/servo/templates/admin/sites/edit_site.html b/servo/templates/admin/sites/edit_site.html
new file mode 100644
index 0000000..5c0004a
--- /dev/null
+++ b/servo/templates/admin/sites/edit_site.html
@@ -0,0 +1,9 @@
+{% extends "admin/sites/index.html" %}
+
+{% block detail_block %}
+ <form method="post" action="" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ {% include "form_buttons.html" %}
+ </form>
+{% endblock detail_block %}
diff --git a/servo/templates/admin/sites/index.html b/servo/templates/admin/sites/index.html
new file mode 100644
index 0000000..4cf96f3
--- /dev/null
+++ b/servo/templates/admin/sites/index.html
@@ -0,0 +1,23 @@
+{% extends "admin/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'admin-create_site' %}" class="btn btn-inverse"><i class="fa fa-plus icon-white"></i> {% trans "New Site" %}</a>
+{% endblock toolbar %}
+
+{% block second_column %}
+<div class="span3">
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Sites" %}</li>
+ {% for i in sites %}
+ <li class="{% active request "sites" i.pk %}"><a href="{% url 'admin-edit_site' i.pk %}">{{ i.name }}</a></li>
+ {% endfor %}
+ </ul>
+</div>
+<div class="span9">
+ {% block detail_block %}
+ <h1 class="text-muted">{% trans "No site selected" %}</h1>
+ {% endblock detail_block %}
+</div>
+{% endblock second_column %}
diff --git a/servo/templates/admin/statuses/form.html b/servo/templates/admin/statuses/form.html
new file mode 100644
index 0000000..7998ddf
--- /dev/null
+++ b/servo/templates/admin/statuses/form.html
@@ -0,0 +1,21 @@
+{% extends "admin/statuses/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#" data-toggle="tab">{% trans "General" %}</a></li>
+</ul>
+
+<form method="post" action="" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="form-actions">
+ {% if status.pk %}
+ <a class="btn btn-danger" data-modal="#modal" href="{% url 'admin-delete_status' status.pk %}">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/statuses/index.html b/servo/templates/admin/statuses/index.html
new file mode 100644
index 0000000..01df5fa
--- /dev/null
+++ b/servo/templates/admin/statuses/index.html
@@ -0,0 +1,6 @@
+{% extends "generic/admin_list.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'admin-create_status' %}" class="btn btn-default navbar-btn"><i class="icon-plus"></i> {% trans "New Status" %}</a>
+{% endblock toolbar %}
diff --git a/servo/templates/admin/statuses/remove.html b/servo/templates/admin/statuses/remove.html
new file mode 100644
index 0000000..78385a6
--- /dev/null
+++ b/servo/templates/admin/statuses/remove.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ <h2>{% blocktrans with status.title as title %}Delete status "{{ title }}"?{% endblocktrans %}</h2>
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans "This action cannot be undone." %}</p>
+{% endblock body %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/admin/tags/form.html b/servo/templates/admin/tags/form.html
new file mode 100644
index 0000000..8f6d975
--- /dev/null
+++ b/servo/templates/admin/tags/form.html
@@ -0,0 +1,17 @@
+{% extends "admin/tags/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<form action="" method="post" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="form-actions">
+ {% if tag.pk %}
+ <a class="btn btn-danger" data-modal="#modal" href="{% url 'admin-delete_tag' tag.pk %}">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/tags/index.html b/servo/templates/admin/tags/index.html
new file mode 100644
index 0000000..26ff6bd
--- /dev/null
+++ b/servo/templates/admin/tags/index.html
@@ -0,0 +1,37 @@
+{% extends "generic/admin_list.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+<a href="{% url 'admin-create_tag' type=type %}" class="btn btn-default navbar-btn"><i class="icon-plus"></i> {% trans "New Tag" %}</a>
+{% endblock toolbar %}
+
+{% block second_column %}
+
+<div class="row">
+ <ul class="nav nav-tabs">
+ {% for k, v in types %}
+ <li {% if k == type %} class="active"{% endif %}><a href="{% url 'admin-tags' type=k %}">{{ v }}</a></li>
+ {% endfor %}
+ </ul>
+</div>
+
+<div class="row">
+ <div class="span5">
+ <ul class="nav nav-pills nav-stacked">
+ {% for o in object_list %}
+ <li class="{% active_url request o.get_admin_url %}"><a href="{{ o.get_admin_url }}">{{ o.title }}</a></li>
+ {% empty %}
+ <li class="empty">{% blocktrans with objects=title %}No {{ objects }} found.{% endblocktrans %}</li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="span7">
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ </ul>
+ {% block third_column %}
+ {% endblock third_column %}
+ </div>
+</div>
+{% endblock second_column %}
diff --git a/servo/templates/admin/templates/form.html b/servo/templates/admin/templates/form.html
new file mode 100644
index 0000000..a55803a
--- /dev/null
+++ b/servo/templates/admin/templates/form.html
@@ -0,0 +1,21 @@
+{% extends "admin/templates/list_templates.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#" data-toggle="tab">{% trans "General" %}</a></li>
+</ul>
+
+<form method="post" action="" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="form-actions">
+ {% if template.pk %}
+ <a class="btn btn-danger" data-modal="#modal" href="{% url 'admin-delete_template' template.pk %}">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/templates/list_templates.html b/servo/templates/admin/templates/list_templates.html
new file mode 100644
index 0000000..3af70d3
--- /dev/null
+++ b/servo/templates/admin/templates/list_templates.html
@@ -0,0 +1,7 @@
+{% extends "generic/admin_list.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a class="btn btn-default navbar-btn" href="{% url 'admin-edit_template' %}"><i class="icon-plus"></i> {% trans "New Template" %}</a>
+{% endblock toolbar %}
diff --git a/servo/templates/admin/users/delete_group.html b/servo/templates/admin/users/delete_group.html
new file mode 100644
index 0000000..2c733df
--- /dev/null
+++ b/servo/templates/admin/users/delete_group.html
@@ -0,0 +1,11 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}{% trans "Really delete this group?" %}{% endblock header %}
+{% block body %}{% trans "Group member accounts will not be deleted." %}{% endblock body %}
+{% block footer %}
+<form action="{{ action }}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+</form>
+{% endblock footer %}
diff --git a/servo/templates/admin/users/form.html b/servo/templates/admin/users/form.html
new file mode 100644
index 0000000..34764ab
--- /dev/null
+++ b/servo/templates/admin/users/form.html
@@ -0,0 +1,70 @@
+{% extends "admin/users/index.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<form action="" method="post" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Permissions" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "Location" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "Profile" %}</a></li>
+ <li><a href="#tab5" data-toggle="tab">{% trans "GSX" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.first_name %}
+ {% include "form_field_snippet.html" with field=form.last_name %}
+ {% include "form_field_snippet.html" with field=form.username %}
+ {% include "form_field_snippet.html" with field=form.email %}
+ {% include "form_field_snippet.html" with field=form.password1 %}
+ {% include "form_field_snippet.html" with field=form.password2 %}
+ {% include "form_field_snippet.html" with field=form.is_active %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "form_field_snippet.html" with field=form.groups %}
+ {% include "form_field_snippet.html" with field=form.is_staff %}
+ {% include "form_field_snippet.html" with field=form.customer %}
+ <div class="control-group ">
+ <label class="control-label">{% trans "API Tokens" %}</label>
+ <div class="controls">
+ <ul style="padding:5px">
+ {% for t in user.get_tokens %}
+ <li class="muted">{{ t.key }}<a href="{% url 'admin-delete_user_token' user.pk %}"><i class="icon-trash"></i></a></li>
+ {% endfor %}
+ </ul>
+ {% if user.pk %}
+ <a class="btn btn-default btn-small" href="{% url 'admin-create_user_token' user.pk %}">{% trans "New Token" %}</a>
+ {% else %}
+ <a class="btn btn-default btn-small disabled" href="#">{% trans "New Token" %}</a>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="tab3">
+ {% include "form_field_snippet.html" with field=form.location %}
+ {% include "form_field_snippet.html" with field=form.locations %}
+ </div>
+ <div class="tab-pane" id="tab4">
+ {% include "form_field_snippet.html" with field=form.locale %}
+ {% include "form_field_snippet.html" with field=form.region %}
+ {% include "form_field_snippet.html" with field=form.timezone %}
+ {% include "form_field_snippet.html" with field=form.queues %}
+ </div>
+ <div class="tab-pane" id="tab5">
+ {% include "form_field_snippet.html" with field=form.tech_id %}
+ {% include "form_field_snippet.html" with field=form.gsx_userid %}
+ {% include "form_field_snippet.html" with field=form.gsx_poprefix %}
+ </div>
+ </div>
+ <div class="form-actions">
+ {% if user.pk %}
+ <a href="{% url 'admin-delete_user' user.pk %}" class="btn btn-danger" data-modal="#modal">{% trans "Delete" %}</a>
+ {% else %}
+ <a href="#" class="btn btn-danger disabled">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="button" class="btn" onclick="javascript:history.back();">{% trans "Back" %}</button>
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/admin/users/group_form.html b/servo/templates/admin/users/group_form.html
new file mode 100644
index 0000000..07b9785
--- /dev/null
+++ b/servo/templates/admin/users/group_form.html
@@ -0,0 +1,23 @@
+{% extends "admin/users/groups.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block third_column %}
+<div class="row-fluid">
+ <div class="span3">
+ <ul class="nav nav-pills nav-stacked">
+ {% for o in object_list %}
+ <li class="{% active_url request o.get_admin_url %}"><a href="{% url 'admin-edit_group' o.pk %}">{{ o.name }}</a></li>
+ {% endfor %}
+ </ul>
+ </ul>
+ </div>
+ <div class="span9">
+ <form action="" method="post" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ {% include "form_buttons.html" %}
+ </form>
+ </div>
+</div>
+{% endblock third_column %}
diff --git a/servo/templates/admin/users/groups.html b/servo/templates/admin/users/groups.html
new file mode 100644
index 0000000..0d81c3b
--- /dev/null
+++ b/servo/templates/admin/users/groups.html
@@ -0,0 +1,37 @@
+{% extends "admin/users/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block second_column %}
+{% include "admin/users/tabs.html" %}
+
+{% block third_column %}
+<table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Group" %}</th>
+ <th>{% trans "Members" %}</th>
+ <th style="width:100px"></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for group in object_list %}
+ <tr>
+ <td><a href="{% url 'admin-edit_group' group.pk %}">{{ group.name }}</a></td>
+ <td>{{ group.user_set.all|join:", "}}</td>
+ <td style="text-align:right">
+ <div class="btn-group">
+ <a href="{% url 'admin-edit_group' group.pk %}" class="btn btn-warning"><i class="icon-pencil icon-white"></i></a>
+ <a href="{% url 'admin-delete_group' group.pk %}" class="btn btn-danger" data-modal="#modal"><i class="icon-trash icon-white"></i></a>
+ </div>
+ </td>
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="3" class="text-muted empty">{% trans "No groups defined" %}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock third_column %}
+{% endblock second_column %}
diff --git a/servo/templates/admin/users/index.html b/servo/templates/admin/users/index.html
new file mode 100644
index 0000000..c85ced1
--- /dev/null
+++ b/servo/templates/admin/users/index.html
@@ -0,0 +1,51 @@
+{% extends "admin/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+<div class="btn-group">
+ <a class="btn" href="{% url 'admin-create_user' %}"><i class="icon-plus"></i> {% trans "New User" %}</a>
+ <a class="btn btn-default navbar-btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'admin-create_group' %}">{% trans "New Group" %}</a></li>
+ <li><a href="{% url 'admin-upload_users' %}" data-modal="#modal">{% trans "Upload Users" %}</a></li>
+ </ul>
+</div>
+{% endblock toolbar %}
+
+{% block second_column %}
+<div class="row-fluid">
+ {% include "admin/users/tabs.html" %}
+</div>
+
+<div class="row-fluid">
+ <div class="span3">
+ <div class="btn-group">
+ <a class="btn btn-mini dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-search"></i>
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% for l in locations %}
+ <li><a href="?l={{ l.pk|safe }}">{{ l }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ <ul class="nav nav-pills nav-stacked">
+ {% for o in object_list %}
+ {% if o.is_active %}
+ <li class="{% active_url request o.get_admin_url %}"><a href="{{ o.get_admin_url }}"><i class="{{ o.get_icon }}"></i> {{ o.get_name|truncatechars:"25" }}</a></li>
+ {% else %}
+ <li class="{% active_url request o.get_admin_url %}"><a href="{{ o.get_admin_url }}" class="muted"><i class="{{ o.get_icon }}"></i> {{ o.get_name }}</a></li>
+ {% endif %}
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="span9">
+ {% block third_column %}
+ {% endblock third_column %}
+ </div>
+</div>
+{% endblock second_column %}
diff --git a/servo/templates/admin/users/remove.html b/servo/templates/admin/users/remove.html
new file mode 100644
index 0000000..5c80d29
--- /dev/null
+++ b/servo/templates/admin/users/remove.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with user=user.get_full_name %}Delete user {{ user }}?{% endblocktrans %}
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans 'This action cannot be undone.' %}</p>
+{% endblock body %}
+
+{% block footer %}
+ <form action="{% url 'admin-delete_user' user.id %}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/admin/users/tabs.html b/servo/templates/admin/users/tabs.html
new file mode 100644
index 0000000..576e5da
--- /dev/null
+++ b/servo/templates/admin/users/tabs.html
@@ -0,0 +1,7 @@
+{% load i18n %}
+{% load servo_tags %}
+
+<ul class="nav nav-tabs">
+ <li class="{% active request "users" %}"><a href="{% url 'admin-list_users' %}">{% trans "Users" %}</a></li>
+ <li class="{% active request "groups" %}"><a href="{% url 'admin-list_groups' %}">{% trans "Groups" %}</a></li>
+</ul>
diff --git a/servo/templates/admin/users/upload_users.html b/servo/templates/admin/users/upload_users.html
new file mode 100644
index 0000000..746842b
--- /dev/null
+++ b/servo/templates/admin/users/upload_users.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block body %}
+<form method="post" action="{{ action }}" enctype="multipart/form-data" class="form-horizontal" accept-charset="utf-8">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+</form>
+{% endblock body %}
diff --git a/servo/templates/checkin/confirmation.html b/servo/templates/checkin/confirmation.html
new file mode 100644
index 0000000..00125c3
--- /dev/null
+++ b/servo/templates/checkin/confirmation.html
@@ -0,0 +1,13 @@
+{% extends "checkin/index.html" %}
+{% load i18n %}
+
+{% block main %}
+<div class="page-header">
+ <h2>{% trans "Confirmation" %}</h2>
+</div>
+<form method="post" action="">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
+</form>
+{% endblock main %}
diff --git a/servo/templates/checkin/customer_form.html b/servo/templates/checkin/customer_form.html
new file mode 100644
index 0000000..305c67a
--- /dev/null
+++ b/servo/templates/checkin/customer_form.html
@@ -0,0 +1,87 @@
+{% load i18n %}
+{% load bootstrap3 %}
+
+<div class="row">
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.phone %}
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.email %}
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.fname %}
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.lname %}
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.address %}
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.company %}
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <div class="row">
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.postal_code %}
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.city %}
+ </div>
+ </div>
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field customer_form.country %}
+ </div>
+</div>
+{% comment %}
+<div class="row">
+ <div class="col-md-12">
+ {% bootstrap_field customer_form.notify_by_sms %}
+ {% bootstrap_field customer_form.notify_by_email %}
+ </div>
+</div>
+{% endcomment %}
+
+{% block media %}
+{% if request.user.is_authenticated %}
+<script type="text/javascript">
+
+ function SearchViewModel() {
+ var self = this;
+ self.results = ko.observableArray();
+
+ self.loadCustomer = function(c){
+ $.get('{% url "checkin-get_customer" %}?c=' + c.id, function(r){
+ $('#id_fname').val(r.fname);
+ $('#id_lname').val(r.lname);
+ $('#id_address').val(r.address);
+ $('#id_postal_code').val(r.postal_code);
+ $('#id_city').val(r.city);
+ $('#id_country').val(r.country);
+ $('#id_email').val(r.email);
+ $('#modal').modal('hide');
+ });
+ }
+ }
+
+ var svm = new SearchViewModel();
+ ko.applyBindings(svm);
+
+ $('#id_phone').next('span').on('click', function(){
+ var q = $('#id_phone').val();
+ $.get('?phone=' + q, function(r){
+ svm.results(r);
+ $('#modal').modal();
+ });
+ });
+</script>
+{% endif %}
+{% endblock media %}
diff --git a/servo/templates/checkin/device_form.html b/servo/templates/checkin/device_form.html
new file mode 100644
index 0000000..16450e7
--- /dev/null
+++ b/servo/templates/checkin/device_form.html
@@ -0,0 +1,62 @@
+{% load i18n %}
+{% load bootstrap3 %}
+
+{% if device.fmip_active %}
+ <div class="alert alert-danger" role="alert">
+ {% trans "Find My iPhone is active!" %}
+ <button type="button" class="close" data-dismiss="alert" aria-label="{% trans "Close" %}"><span aria-hidden="true">&times;</span></button>
+ </div>
+{% endif %}
+
+{% if error %}
+ <div class="alert alert-danger" role="alert">
+ {{ error }}
+ <button type="button" class="close" data-dismiss="alert" aria-label="{% trans "Close" %}"><span aria-hidden="true">&times;</span></button>
+ </div>
+{% endif %}
+
+<div class="col-md-4">
+ <img src="{{ device.get_image_url }}" alt="{{ device.description }}" class="device-image"/>
+</div>
+<div class="col-md-8">
+ <div class="row">
+ <div class="col-md-6">
+ {% bootstrap_field device_form.sn %}
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field device_form.description %}
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label" for="id_warranty_status">{% trans "Warranty Status" %}</label>
+ <p>{{ device.get_warranty_status_display }}</p>
+ </div>
+ {% bootstrap_field device_form.purchased_on %}
+ {% bootstrap_field device_form.purchase_country %}
+ <div class="row">
+ <div class="col-md-6">
+ {% if device.is_mac %}
+ {% bootstrap_field device_form.username %}
+ {% endif %}
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field device_form.password %}
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ {% bootstrap_field device_form.accessories %}
+ </div>
+ </div>
+</div>
+<script type="text/javascript">
+ $('#id_sn').next('span').on('click', function(){
+ var v = $('#id_sn').val().trim();
+ if (v.length < 3) return false;
+ var target = document.getElementById('main');
+ var spinner = new Spinner().spin(target);
+ $('#device_details').load('?sn='+v, function(r){
+ spinner.stop();
+ });
+ });
+</script> \ No newline at end of file
diff --git a/servo/templates/checkin/error.html b/servo/templates/checkin/error.html
new file mode 100644
index 0000000..eaa45d5
--- /dev/null
+++ b/servo/templates/checkin/error.html
@@ -0,0 +1,15 @@
+{% extends "checkin/index.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "An error occurred" %}{% endblock title %}
+
+{% block main %}
+<div class="container-narrow">
+ <div class="jumbotron">
+ <h1>{% trans "Oops!" %}</h1>
+ <p class="lead">{% trans "It appears that an error has occurred." %}</p>
+ <p>{{ message }}</p>
+ <a class="btn btn-large btn-success" href="{% url 'checkin-index' %}"><i class="icon-refresh icon-white"></i> {% trans "Try again" %}</a>
+ </div>
+</div>
+{% endblock main %}
diff --git a/servo/templates/checkin/index.html b/servo/templates/checkin/index.html
new file mode 100644
index 0000000..9ecc884
--- /dev/null
+++ b/servo/templates/checkin/index.html
@@ -0,0 +1,95 @@
+{% load i18n %}{% load bootstrap3 %}<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>{% block title %}{{ title }}{% endblock title %}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link href="{{ STATIC_URL }}css/checkin.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}bs3/css/timeline.css" rel="stylesheet">
+ <link rel="stylesheet" href="{{ STATIC_URL }}bs3/css/bootstrap.min.css">
+ <link rel="stylesheet" href="{{ STATIC_URL }}bs3/css/bootstrap-theme.min.css">
+ <script src="{{ STATIC_URL }}js/jquery.min.js"></script>
+ <script src="{{ STATIC_URL }}bs3/js/bootstrap.min.js"></script>
+ <script src="{{ STATIC_URL }}js/knockout.js"></script>
+ </head>
+ <body id="main">
+ <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+ <div class="container">
+ <div class="collapse navbar-collapse">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ {% if request.user.is_authenticated %}
+ <ul class="nav navbar-nav">
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">{% bootstrap_icon "map-marker" %} {{ request.session.checkin_location_name }} <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ {% for l in request.session.checkin_locations %}
+ <li{% if l.pk == request.session.checkin_location %} class="active"{% endif %}><a href="?l={{ l.pk|safe }}">{{ l.name }}</a></li>
+ {% endfor %}
+ </ul>
+ </li>
+ </ul>
+ {% endif %}
+ <ul class="nav navbar-nav pull-right">
+ {% if request.user.is_authenticated %}
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">{% bootstrap_icon "user" %} {{ request.session.checkin_user_name }} <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ {% for u in request.session.checkin_users %}
+ <li{% if u.pk == request.session.checkin_user %} class="active"{% endif %}><a href="?u={{ u.pk|safe }}">{{ u.name }}</a></li>
+ {% endfor %}
+ <li class="divider"></li>
+ <li><a href="{% url 'accounts-list_orders' user.username %}">{% bootstrap_icon "log-in" %} {% trans "Go to Servo" %}</a></li>
+ <li><a href="{% url 'accounts-logout' %}" data-modal="#modal">{% bootstrap_icon "off" %} {% trans "Log out" %}...</a></li>
+ </ul>
+ </li>
+ {% endif %}
+ </ul>
+ </div><!--/.nav-collapse -->
+ </div>
+ </div>
+ </div>
+ <div class="main container">
+ {% if request.user.is_authenticated %}
+ <div class="modal fade" id="modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">{% trans "Search results" %}</h4>
+ </div>
+ <div class="modal-body">
+ <ul class="nav nav-pills nav-stacked" data-bind="foreach: results">
+ <li><a href="#" data-bind="text: title, click: $parent.loadCustomer"></a></li>
+ </ul>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ <div class="row">
+ <div class="col-md-12">
+ {% bootstrap_messages %}
+ {% block main %}(no content){% endblock main %}
+ </div>
+ </div>
+ </div><!-- /container -->
+ <script type="text/javascript" src="{{ STATIC_URL }}js/spin.js-2.1.0.min.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/jquery.spin.new.js"></script>
+ <script src="{{ STATIC_URL }}js/common.js"></script>
+ {% block media %}{% endblock media %}
+ <script type="text/javascript">
+ $(function(){
+ $( "#id_agree_to_terms" ).click(function() {
+ $('#id_btn_submit').prop('disabled', !$(this).prop('checked'));
+ });
+ });
+ </script>
+ </body>
+</html>
diff --git a/servo/templates/checkin/newindex.html b/servo/templates/checkin/newindex.html
new file mode 100644
index 0000000..41a6a05
--- /dev/null
+++ b/servo/templates/checkin/newindex.html
@@ -0,0 +1,146 @@
+{% extends "checkin/index.html" %}
+{% load servo_tags %}
+{% load bootstrap3 %}
+{% load i18n %}
+
+{% block main %}
+
+
+<div class="page-header">
+ <h2>{% trans "Device" %}</h2>
+</div>
+
+{% comment %}
+ <div class="container choose-device">
+ <div class="row">
+ <div class="col-md-3">
+ <a href="{% url 'checkin-new' %}?d=mac">
+ <img src="{{ STATIC_URL }}images/checkin/mac.jpg" alt="Mac"/>
+ <p>{% trans "Mac" %}</p>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a href="{% url 'checkin-new' %}?d=iphone">
+ <img src="{{ STATIC_URL }}images/checkin/iphone.jpg" alt="iPhone"/>
+ <p>{% trans "iPhone" %}</p>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a href="{% url 'checkin-new' %}?d=ipad">
+ <img src="{{ STATIC_URL }}images/checkin/ipad.jpg" alt="iPad"/>
+ <p>{% trans "iPad" %}</p>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a href="{% url 'checkin-new' %}?d=ipod">
+ <img src="{{ STATIC_URL }}images/checkin/ipod.jpg" alt="iPod"/>
+ <p>{% trans "iPod" %}</p>
+ </a>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">
+ <a href="{% url 'checkin-new' %}?d=acc">
+ <img src="{{ STATIC_URL }}images/checkin/appleacc.jpg" alt="Apple Accessory"/>
+ <p>{% trans "Apple Accessory" %}</p>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a href="{% url 'checkin-new' %}?d=beats">
+ <img src="{{ STATIC_URL }}images/checkin/beats.png" alt="Beats Products"/>
+ <p>{% trans "Beats Products" %}</p>
+ </a>
+ </div>
+ <div class="col-md-3">
+ <a href="{% url 'checkin-new' %}?d=other">
+ <img src="{{ STATIC_URL }}images/models/macbookair.jpg" alt="Other Device"/>
+ <p>{% trans "Other Device" %}</p>
+ </a>
+ </div>
+ </div>
+ </div>
+{% endcomment %}
+<div class="container">
+ <form action="{% url 'checkin-index' %}" method="post" enctype="multipart/form-data">
+ {% csrf_token %}
+ <div class="row" id="device_details">
+ {% include "checkin/device_form.html" %}
+ </div>
+ <div class="page-header">
+ <h2>{% trans "Customer" %}</h2>
+ </div>
+ <div id="customer">
+ {% include "checkin/customer_form.html" %}
+ </div>
+ <div class="page-header">
+ <h2>{% trans "Problem description" %}</h2>
+ </div>
+ {% if request.user.is_authenticated %}
+ <div class="row">
+ <div class="col-md-12">
+ <div class="form-group">
+ <label class="control-label">{% trans "Tags" %}</label>
+ <div class="row bootstrap3-multi-input select-tags">
+ {% for t in tags %}
+ <div class="btn-group" data-toggle="buttons">
+ <label class="btn btn-sm btn-{{ t.color|replace:"important,danger" }}">
+ <input type="checkbox" name="tags" value="{{ t.id|safe }}"/> {% bootstrap_icon "tag" %} {{ t.title }}
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ <div class="row">
+ <div class="col-md-6">
+ {% bootstrap_field issue_form.issue_description %}
+ {% bootstrap_field issue_form.notes %}
+ {% bootstrap_field device_form.condition %}
+ </div>
+ <div class="col-md-6">
+ {% bootstrap_field device_form.pop %}
+ <table class="table">
+ <thead>
+ <tr>
+ <th></th>
+ <th>{% trans "Yes" %}</th>
+ <th>{% trans "No" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for q in questions %}
+ <tr>
+ <td>
+ <strong>{{ q }}</strong>
+ <p class="small">{{ q.description }}</p>
+ </td>
+ <td><input type="radio" name="__cl__{{ q }}" value="{% trans "Yes" %}"/></td>
+ <td><input type="radio" name="__cl__{{ q }}" value="{% trans "No" %}"/></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% bootstrap_field customer_form.checkin_location %}
+ {% bootstrap_field customer_form.checkout_location %}
+ </div>
+ </div>
+ <hr/>
+ <div class="row">
+ <div class="col-md-12">
+ {% buttons %}
+ {% if not request.user.is_authenticated %}
+ {% bootstrap_field customer_form.agree_to_terms %}
+ {% endif %}
+ {% if request.user.is_authenticated %}
+ <button type="submit" id="id_btn_submit" class="btn btn-primary pull-right">{% trans "Submit" %}</button>
+ {% else %}
+ <button type="submit" id="id_btn_submit" class="btn btn-primary pull-right" disabled="disabled">{% trans "Submit" %}</button>
+ {% endif %}
+ {% endbuttons %}
+ </div>
+ </div>
+ </form>
+</div>
+{% endblock main %}
diff --git a/servo/templates/checkin/status-show.html b/servo/templates/checkin/status-show.html
new file mode 100644
index 0000000..fcd503c
--- /dev/null
+++ b/servo/templates/checkin/status-show.html
@@ -0,0 +1,35 @@
+{% extends "checkin/index.html" %}
+{% load bootstrap3 %}
+{% load humanize %}
+{% load i18n %}
+
+{% block main %}
+ <div class="page-header">
+ <h2>{{ order.status_name }} {{ order.status_started_at|naturaltime|default:"" }}</h2>
+ <p class="lead">{{ order.status.status.description }}</p>
+ </div>
+ {% if timeline|length %}
+ <ul class="timeline">
+ {% for i in timeline %}
+ <li class="{% cycle '' 'timeline-inverted' %}">
+ <div class="timeline-badge {{ i.get_badge }}">{% bootstrap_icon "ok" %}</i></div>
+ <div class="timeline-panel">
+ {% with i.status as status %}
+ <div class="timeline-heading">
+ <h4 class="timeline-title">{{ status.title }}</h4>
+ <p>
+ <small class="text-muted"><i class="fa fa-time"></i> {{ i.started_at|naturaltime }}</small>
+ </p>
+ </div>
+ <div class="timeline-body">
+ <p>{{ status.description }}</p>
+ </div>
+ {% endwith %}
+ </div>
+ </li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ <a href="" class="btn btn-default">{% bootstrap_icon "refresh" %} {% trans "Refresh" %}</a>
+ <a href="{% url 'checkin-index' %}" class="btn btn-primary">{% trans "Return" %}</a>
+{% endblock main %}
diff --git a/servo/templates/checkin/status.html b/servo/templates/checkin/status.html
new file mode 100644
index 0000000..ddb49fb
--- /dev/null
+++ b/servo/templates/checkin/status.html
@@ -0,0 +1,21 @@
+{% extends "checkin/index.html" %}
+{% load bootstrap3 %}
+{% load i18n %}
+
+{% block main %}
+ <div class="page-header">
+ <h2>{% trans "Please enter your Service Order number" %}</h2>
+ </div>
+ <p>{% trans "The Service Order number is an 8-digit code printed on your work confirmation" %}</p>
+ <form method="get" action="" class="nolabel">
+ {% bootstrap_form form %}
+ {% buttons %}
+ <div class="pull-right">
+ <a class="btn btn-default" href="{% url 'checkin-index' %}">{% trans "Back" %}</a>
+ <button type="submit" class="btn btn-primary">
+ {% bootstrap_icon "search" %} Submit
+ </button>
+ </div>
+ {% endbuttons %}
+ </form>
+{% endblock main %}
diff --git a/servo/templates/checkin/terms.html b/servo/templates/checkin/terms.html
new file mode 100644
index 0000000..72d1fa6
--- /dev/null
+++ b/servo/templates/checkin/terms.html
@@ -0,0 +1,9 @@
+{% extends "checkin/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+{% block main %}
+<div class="page-header">
+ <h1>{% blocktrans with company=conf.company_name %}{{ company }} terms of service{% endblocktrans %}</h1>
+</div>
+{{ conf.terms_of_service|markdown }}
+{% endblock main %}
diff --git a/servo/templates/checkin/thanks.html b/servo/templates/checkin/thanks.html
new file mode 100644
index 0000000..ae5b8da
--- /dev/null
+++ b/servo/templates/checkin/thanks.html
@@ -0,0 +1,26 @@
+{% extends "checkin/index.html" %}
+{% load bootstrap3 %}
+{% load i18n %}
+
+{% block main %}
+<div class="page-header">
+ <h2>{% trans "Done!" %}</h2>
+ <p class="lead">{% blocktrans with code=order.code %}Your service order <strong>{{ code }}</strong> has been submitted. {% endblocktrans %} {% blocktrans %}Please click <strong>Print</strong> to print the confirmation.{% endblocktrans %}</p>
+</div>
+<div class="thanks text-center">
+ <img src="{{ STATIC_URL }}images/ok_256.png" alt="Done!"/>
+ <br/>
+ <a href="{% url 'checkin-index' %}" class="btn btn-default btn-large">{% bootstrap_icon "plus" %} {% trans "Create New" %}</a>
+ <a href="{% url 'checkin-print' order.url_code %}" class="btn btn-primary btn-large" target="_blank">{% bootstrap_icon "print" %} {% trans "Print" %}</a>
+</div>
+{% endblock main %}
+
+{% block media %}
+{% comment %}
+<script type="text/javascript">
+ window.setTimeout(function(){
+ window.location = "{% url 'checkin-reset' %}";
+ }, 20*1000);
+</script>
+{% endcomment %}
+{% endblock media %}
diff --git a/servo/templates/customers/choose-list.html b/servo/templates/customers/choose-list.html
new file mode 100755
index 0000000..84efa7d
--- /dev/null
+++ b/servo/templates/customers/choose-list.html
@@ -0,0 +1,8 @@
+{% load i18n %}
+<ul class="nav nav-pills nav-stacked">
+{% for c in customers %}
+ <li><a href="{% url 'orders-select_customer' pk=order_id customer_id=c.id %}"><i class="{{ c.get_icon }}"></i> <strong>{{ c.fullname }}</strong><br/>{{ c.email }}, {{ c.phone }}</a></li>
+{% empty %}
+ <li class="text-center muted">{% trans "No customers found" %}</li>
+{% endfor %}
+</ul>
diff --git a/servo/templates/customers/choose.html b/servo/templates/customers/choose.html
new file mode 100755
index 0000000..cf812a6
--- /dev/null
+++ b/servo/templates/customers/choose.html
@@ -0,0 +1,45 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Search for customer" %}
+{% endblock header %}
+
+{% block body %}
+<form action="{{ action }}" method="post" accept-charset="utf-8" id="search-form" data-target="#search-results" style="margin:0">
+ {% csrf_token %}
+ <div class="input-prepend">
+ <div class="btn-group">
+ <button class="btn dropdown-toggle" data-toggle="dropdown" type="button" style="width:120px">
+ <span id="filter-label">{% trans "Search" %}</span>
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li class="active"><a href="#" data-search="all">{% trans "All" %}</a></li>
+ <li><a href="#" data-search="companies">{% trans "Companies" %}</a></li>
+ <li><a href="#" data-search="contacts">{% trans "People" %}</a></li>
+ </ul>
+ </div>
+ <input type="text" name="name" style="width:390px" autocomplete="off" id="customer_name" placeholder="{% trans "Name, email or phone number" %}"/>
+ <input type="hidden" name="kind" value="all" id="value-kind"/>
+ </div>
+</form>
+<div id="search-results" style="height:250px"><p class="empty muted" style="line-height:250px">{% trans "Enter search query" %}</p></div>
+{% endblock body %}
+
+{% block footer %}
+ <a id="create_customer" href="{% url 'customers-create_customer' group='all' %}" class="btn">{% trans "New Customer" %}</a>
+ <a class="btn btn-primary submit-search" href="#">{% trans "Search" %}</a>
+ <script type="text/javascript">
+ $('.dropdown-menu li a').click(function(e){
+ $('.dropdown-menu li').removeClass('active');
+ $(this).parent().addClass('active');
+ $('#filter-label').text($(this).text());
+ $('#value-kind').val($(this).data('search'));
+ });
+ $('.submit-search').click(function(e){
+ $('#search-form').submit();
+ e.preventDefault();
+ });
+ </script>
+{% endblock footer %}
diff --git a/servo/templates/customers/delete_group.html b/servo/templates/customers/delete_group.html
new file mode 100755
index 0000000..1c06e0f
--- /dev/null
+++ b/servo/templates/customers/delete_group.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Delete customer group?" %}
+{% endblock header %}
+
+{% block body %}
+ {% trans "This action will not delete the customers in this group." %}
+{% endblock body %}
+
+{% block footer %}
+ <form method="post" action="{{ request.path }}">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/customers/edit_group.html b/servo/templates/customers/edit_group.html
new file mode 100755
index 0000000..3f5fe5c
--- /dev/null
+++ b/servo/templates/customers/edit_group.html
@@ -0,0 +1,8 @@
+{% extends "modal.html" %}
+
+{% block body %}
+<form action="{{ request.path }}" method="post" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+</form>
+{% endblock body %}
diff --git a/servo/templates/customers/find.html b/servo/templates/customers/find.html
new file mode 100644
index 0000000..6d8cb14
--- /dev/null
+++ b/servo/templates/customers/find.html
@@ -0,0 +1,95 @@
+{% extends "customers/index.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+<div class="btn-group">
+{% if request.session.customer_query %}
+ <a class="btn" href="{% url 'customers-download' %}"><i class="icon-download-alt"></i> {% trans "Download" %}</a>
+{% else %}
+ <a class="btn disabled" href="#"><i class="icon-download-alt"></i> {% trans "Download" %}</a>
+{% endif %}
+</div>
+{% endblock toolbar %}
+
+{% block tabs %}
+ <li><a href="{% url 'customers-list_all' %}">{% trans "Browse" %}</a></li>
+ <li class="active"><a href="{% url 'customers-find' %}">{% trans "Search" %}</a></li>
+{% endblock tabs %}
+
+{% block customers %}
+<form action="" method="post" class="form-inline well clearfix">
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <div class="control-group pull-right">
+ <label class="control-label">&nbsp;</label>
+ <div class="controls">
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </div>
+</form>
+
+{% if form.errors %}
+ <div class="alert alert-danger">
+ {% for k, v in form.errors.items %}
+ {{ v }}
+ {% endfor %}
+ </div>
+{% endif %}
+
+<table class="table table-hover sortable">
+ <thead>
+ <tr>
+ <th>{% trans "Name" %}</th>
+ <th>{% trans "Email" %}</th>
+ <th>{% trans "Phone" %}</th>
+ <th>{% trans "Address" %}</th>
+ <th>{% trans "Postal Code" %}</th>
+ <th>{% trans "City" %}</th>
+ <th data-defaultsort="disabled"></th>
+ </tr>
+ </thead>
+ <tbody>
+{% for i in customers %}
+ <tr>
+ <td><a href="{{ i.get_absolute_url }}">{{ i.fullname }}</a></td>
+ <td><a href="{% url 'notes-create_to_customer' customer=i.pk %}">{{ i.email }}</a></td>
+ <td>{{ i.phone }}</td>
+ <td>{{ i.street_address }}</td>
+ <td>{{ i.zip_code }}</td>
+ <td>{{ i.city }}</td>
+ <td>
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{{ i.get_absolute_url }}">{% trans "View" %}</a></li>
+ {% if perms.servo.change_customer %}
+ <li><a href="{{ i.get_absolute_url }}edit/">{% trans "Edit" %}</a></li>
+ {% else %}
+ <li><a href="#" class="disabled">{% trans "Edit" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ {% if perms.servo.delete_customer %}
+ <li><a href="{{ i.get_absolute_url }}delete/" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% else %}
+ <li><a href="#" class="disabled"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ </td>
+ </tr>
+{% empty %}
+ <tr>
+ <td class="muted text-center empty" colspan="7">{% trans "No customers found" %}</td>
+ </tr>
+{% endfor %}
+ </tbody>
+ </table>
+ {% include "pagination.html" with items=customers %}
+{% endblock customers %}
diff --git a/servo/templates/customers/form.html b/servo/templates/customers/form.html
new file mode 100755
index 0000000..69c6138
--- /dev/null
+++ b/servo/templates/customers/form.html
@@ -0,0 +1,51 @@
+{% extends "customers/view.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<form method="post" action="" accept-charset="utf-8" class="form-horizontal">
+ {% csrf_token %}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Details" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.name %}
+ {% include "form_field_snippet.html" with field=form.phone %}
+ {% include "form_field_snippet.html" with field=form.email %}
+ {% include "form_field_snippet.html" with field=form.street_address %}
+ {% include "form_field_snippet.html" with field=form.zip_code %}
+ {% include "form_field_snippet.html" with field=form.city %}
+ {% include "form_field_snippet.html" with field=form.is_company %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "form_field_snippet.html" with field=form.parent %}
+ {% include "form_field_snippet.html" with field=form.country %}
+ {% for p in customer.contactinfo_set.all %}
+ <div class="control-group">
+ <select name="keys" class="control-label">
+ <option>{{ p.key }}</option>
+ </select>
+ <div class="controls">
+ <input type="text" name="values" value="{{ p.value }}"/>
+ </div>
+ </div>
+ {% endfor %}
+ <div class="control-group property">
+ <select name="keys" class="control-label" data-value="{{ fields.0.title }}">
+ {% for f in fields %}
+ <option>{{ f.title }}</option>
+ {% endfor %}
+ </select>
+ <div class="controls">
+ <input type="text" name="values"/>
+ </div>
+ </div>
+ {% include "form_field_snippet.html" with field=form.photo %}
+ {% include "form_field_snippet.html" with field=form.groups %}
+ {% include "form_field_snippet.html" with field=form.notes %}
+ </div>
+ </div>
+ {% include "form_buttons.html" %}
+</form>
+{% endblock third_column %}
diff --git a/servo/templates/customers/index.html b/servo/templates/customers/index.html
new file mode 100755
index 0000000..84407d9
--- /dev/null
+++ b/servo/templates/customers/index.html
@@ -0,0 +1,84 @@
+{% extends "default.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+<div class="btn-group">
+ <a class="btn" href="{% url 'customers-create_customer' group=group %}"><i class="icon-plus"></i> {% trans "New Customer" %}</a>
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><span class="caret"></span></a>
+ <ul class="dropdown-menu">
+ {% if customer.is_company %}
+ <li><a href="{% url 'customers-create_contact' parent_id=customer.id %}">{% trans "New Contact" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "New Contact" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a href="{% url 'customers-create_group' %}" data-modal="#modal">{% trans "New Group" %}</a></li>
+ </ul>
+</div>
+<div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'customers-upload' group=group %}" data-modal="#modal">{% trans "Import" %}</a></li>
+ <li><a href="{% url 'customers-download' group=group %}">{% trans "Export" %}</a></li>
+ <li class="divider"></li>
+ {% if group != 'all' %}
+ <li><a href="{% url 'customers-edit_group' group %}" data-modal="#modal">{% trans "Edit Group" %}</a></li>
+ <li><a href="{% url 'customers-delete_group' group %}" data-modal="#modal">{% trans "Delete Group" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Edit Group" %}</a></li>
+ <li class="disabled"><a href="#">{% trans "Delete Group" %}</a></li>
+ {% endif %}
+ </ul>
+</div>
+{% endblock toolbar %}
+
+{% block content %}
+
+<div class="row-fluid">
+ <div class="span12">
+ <ul class="nav nav-tabs" style="margin-top:30px">
+ {% block tabs %}
+ <li class="active"><a href="{% url 'customers-list_all' %}">{% trans "Browse" %}</a></li>
+ <li><a href="{% url 'customers-find' %}">{% trans "Search" %}</a></li>
+ {% endblock tabs %}
+ </ul>
+ </div>
+</div>
+
+<div class="row-fluid">
+{% block customers %}
+ <div class="span2">
+ {% block first_column %}
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Groups" %}</li>
+ <li {% if group == 'all' %}class="active"{% endif %}>
+ <a href="{% url 'customers-list_all' %}">{% trans "All" %}<span class="badge pull-right">{{ customer_count }}</span></a>
+ </li>
+ {% for i in groups %}
+ <li class="{% active request i.slug "/" %}">
+ <a href="{% url 'customers-list' group=i.slug %}">{{ i.name|truncatechars:16 }}<span class="badge pull-right">{{ i.customer_set.all.count }}</span></a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% endblock first_column %}
+ </div>
+ <div class="span3">
+ {% block second_column %}
+ {% include "customers/list.html" %}
+ {% endblock second_column %}
+ </div>
+ <div class="span7">
+ {% block third_column %}
+ <h2 class="muted text-center">{% trans "No customer selected" %}</h2>
+ {% endblock third_column %}
+ </div>
+{% endblock customers %}
+</div>
+{% endblock content %}
+
+{% block crumbs %}
+<li class="active">{% trans "Customers" %}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/customers/list.html b/servo/templates/customers/list.html
new file mode 100755
index 0000000..ee1e926
--- /dev/null
+++ b/servo/templates/customers/list.html
@@ -0,0 +1,11 @@
+{% load servo_tags %}
+{% load i18n %}
+
+<ul class="nav nav-list">
+ <li class="nav-header">{% trans "Customers" %}</li>
+{% for node in customers %}
+ <li class="{% active request group.slug node.pk %}"><a href="{% url 'customers-view_customer' group=group pk=node.pk %}"><i class="icon {{ node.get_icon }}"></i> {{ node.name }}</a>
+ </li>
+{% endfor %}
+</ul>
+{% include "pagination.html" with items=customers %}
diff --git a/servo/templates/customers/merge.html b/servo/templates/customers/merge.html
new file mode 100644
index 0000000..942286d
--- /dev/null
+++ b/servo/templates/customers/merge.html
@@ -0,0 +1,19 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block body %}
+<form action="{% url 'customers-merge_customer' pk=customer.pk %}" method="post" accept-charset="utf-8" id="search-form" data-target="#search-results">
+ {% csrf_token %}
+ <input type="hidden" name="id" value="{{ customer.pk|safe }}"/>
+ <input type="text" class="search-query" name="name" autocomplete="off" placeholder="{% trans "Customer name" %}"/>
+</form>
+<div id="search-results"></div>
+{% endblock body %}
+
+{% block footer %}
+<button class="btn btn-primary" data-dismiss="modal">{% trans "Close" %}</button>
+{% endblock footer %}
diff --git a/servo/templates/customers/move.html b/servo/templates/customers/move.html
new file mode 100755
index 0000000..8bed17b
--- /dev/null
+++ b/servo/templates/customers/move.html
@@ -0,0 +1,20 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Move customer" %}
+{% endblock header %}
+
+{% block body %}
+<form action="{% url 'customers-filter' %}" method="post" accept-charset="utf-8" id="search-form" data-target="#search-results">
+ {% csrf_token %}
+ <input type="hidden" name="id" value="{{ customer.pk|safe }}"/>
+ <input type="text" class="search-query" name="name" autocomplete="off" placeholder="{% trans "Customer name" %}"/>
+</form>
+<div id="search-results"></div>
+{% endblock body %}
+
+{% block footer %}
+<a href="{% url 'customers-move_customer' pk=customer.pk new_parent=0 %}" class="btn">{% trans "Move to top" %}</a>
+<button class="btn btn-primary" data-dismiss="modal">{% trans "Close" %}</button>
+{% endblock footer %}
diff --git a/servo/templates/customers/remove.html b/servo/templates/customers/remove.html
new file mode 100755
index 0000000..faa2cbc
--- /dev/null
+++ b/servo/templates/customers/remove.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans 'Delete' %} {{ customer.name }}?
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans "This will also delete all customer's contacts. Customer's orders will not be deleted" %}</p>
+{% endblock body %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/customers/results-merge.html b/servo/templates/customers/results-merge.html
new file mode 100644
index 0000000..a3c6a43
--- /dev/null
+++ b/servo/templates/customers/results-merge.html
@@ -0,0 +1,5 @@
+<ul class="nav nav-pills nav-stacked">
+{% for c in results %}
+ <li><a href="{% url 'customers-merge_customer' pk=customer.pk target=c.pk %}">{{ c.name }}</a></li>
+{% endfor %}
+</ul>
diff --git a/servo/templates/customers/search-results.html b/servo/templates/customers/search-results.html
new file mode 100755
index 0000000..2af581e
--- /dev/null
+++ b/servo/templates/customers/search-results.html
@@ -0,0 +1,5 @@
+<ul class="nav nav-pills nav-stacked">
+{% for c in results %}
+ <li><a href="{% url 'customers-move_customer' pk=id new_parent=c.id %}">{{ c.name }}</a></li>
+{% endfor %}
+</ul>
diff --git a/servo/templates/customers/search.html b/servo/templates/customers/search.html
new file mode 100755
index 0000000..39cccb8
--- /dev/null
+++ b/servo/templates/customers/search.html
@@ -0,0 +1,44 @@
+{% extends "two_column_layout.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+<a href="{% url 'customers-create_customer' group='all' %}?name={{ query }}" class="btn"><i class="icon-plus"></i> {% trans "New Customer" %}</a>
+{% endblock toolbar %}
+
+{% block first_column %}
+<ul class="nav nav-list">
+ <li class="nav-header">{% trans "Show" %}</li>
+ <li{% if not kind %} class="active"{% endif %}><a href="{% url 'customers-search' %}?q={{ query }}">{% trans "All" %}</a></li>
+ <li{% if kind == 'company' %} class="active"{% endif %}><a href="{% url 'customers-search' %}?q={{ query }}&amp;kind=company">{% trans "Companies" %}</a></li>
+ <li{% if kind == 'contact' %} class="active"{% endif %}><a href="{% url 'customers-search' %}?q={{ query }}&amp;kind=contact">{% trans "People" %}</a></li>
+</ul>
+{% endblock first_column %}
+
+{% block second_column %}
+{% if customers %}
+{% for i in customers %}
+ <address>
+ <strong><a href="{{ i.get_absolute_url }}">{{ i.fullname }}</a></strong><br>
+ {% if i.street_address %}
+ {{ i.street_address }}<br/>
+ {{ i.zip_code }}, {{ i.city }}<br/>
+ {% endif %}
+ {% if i.phone %}
+ <abbr title="{% trans "Phone" %}">P:</abbr> {{ i.phone }}
+ {% endif %}
+ {% if i.email %}
+ <br/>
+ <a href="{% url 'notes-create_to_customer' customer=i.pk %}"><i class="icon-envelope"></i> {{ i.email }}</a>
+ {% endif %}
+ </address>
+ <hr/>
+{% endfor %}
+{% else %}
+ <h1 class="muted text-center">{% trans "No customers found" %}</h1>
+{% endif %}
+{% endblock second_column %}
+
+{% block crumbs %}
+ <li><a href="{% url 'customers-list_all' %}">{% trans "Customers" %}</a> <span class="divider">/</span></li>
+ <li class="active">{{ title }}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/customers/upload.html b/servo/templates/customers/upload.html
new file mode 100644
index 0000000..9c5447c
--- /dev/null
+++ b/servo/templates/customers/upload.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Import customer data" %}
+{% endblock header %}
+
+{% block body %}
+<form action="{{ action }}" method="post" enctype="multipart/form-data" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+</form>
+{% endblock body %}
diff --git a/servo/templates/customers/view.html b/servo/templates/customers/view.html
new file mode 100755
index 0000000..6e28643
--- /dev/null
+++ b/servo/templates/customers/view.html
@@ -0,0 +1,119 @@
+{% extends "customers/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block third_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Customer" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Orders" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "Devices" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "Messages" %}</a></li>
+</ul>
+<div class="tab-content customer-view">
+ <div class="tab-pane active" id="tab1">
+ <legend>
+ {{ customer.name }}
+ {% if customer.parent %}<br/>
+ <small class="muted">{{ customer.parent.name }} <a href="{{ customer.parent.get_absolute_url }}"><i class="icon icon-circle-arrow-right"></i></a></small>
+ {% endif %}
+ </legend>
+ <dl class="dl-horizontal">
+ {% if customer.email %}
+ <dt>{% trans "Email" %}</dt>
+ <dd><a href="{% url 'notes-create_with_recipient' recipient=customer.email %}">{{ customer.email }}</a></dd>
+ {% endif %}
+ {% if customer.phone %}
+ <dt>{% trans "Phone" %}</dt>
+ <dd><a href="{% url 'notes-create_with_recipient' recipient=customer.get_standard_phone %}">{{ customer.get_international_phone }}</a></dd>
+ {% endif %}
+ {% if customer.street_address %}
+ <dt>{% trans "Address" %}</dt>
+ <dd>{{ customer.street_address }}<br/>{{ customer.zip_code }} {{ customer.city }}</dd>
+ {% endif %}
+ {% for ci in customer.contactinfo_set.all %}
+ <dt>{{ ci.key }}</dt>
+ <dd>{{ ci.value }}</dd>
+ {% endfor %}
+ {% if customer.country %}
+ <dt>{% trans "Country" %}</dt>
+ <dd>{{ customer.get_country_display }}</dd>
+ {% endif %}
+ {% if customer.notes %}
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ customer.notes }}</dd>
+ {% endif %}
+ </dl>
+ {% if customer.is_company %}
+ <dl class="dl-horizontal">
+ <dt>{% trans "Contacts" %}</dt>
+ <dd>{% for c in customer.get_contacts %}{{ c }} <a href="{{ c.get_absolute_url }}"><i class="icon icon-circle-arrow-right"></i></a><br/>{% endfor %}</dd>
+ </dl>
+ {% endif %}
+ <hr/>
+ {% with request.session.current_order_id as order_id %}
+ {% if order_id and not request.session.current_order_customer %}
+ <a href="{% url 'customers-add_to_order' customer.pk order_id %}" class="btn btn-primary"><i class="icon-share-alt icon-white"></i> {% trans "Use in " %} #{{ request.session.current_order_code }}</a>
+ {% else %}
+ <a href="#" class="btn btn-primary disabled"><i class="icon-share-alt icon-white"></i>{% trans "Use in current order" %}</a>
+ {% endif %}
+ {% endwith %}
+ <div class="btn-group">
+ <a href="{% url 'orders-create_with_customer' customer_id=customer.pk %}" class="btn">{% trans "Create Service Order" %}</a>
+ </div>
+ <div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if perms.servo.change_customer %}
+ <li><a href="{% url 'customers-move_customer' pk=customer.pk %}" data-modal="#modal">{% trans "Move Customer" %}</a></li>
+ <li><a href="{% url 'customers-merge_customer' pk=customer.pk %}" data-modal="#modal">{% trans "Merge Customer" %}</a></li>
+ {% else %}
+ <li><a href="#" class="disabled">{% trans "Move Customer" %}</a></li>
+ <li><a href="#" class="disabled">{% trans "Merge Customer" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ {% if perms.servo.delete_customer %}
+ <li><a href="delete/" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% else %}
+ <li><a href="#" class="disabled"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ <div class="pull-right">
+ {% if perms.servo.change_customer %}
+ <a href="edit/" class="btn">{% trans "Edit" %}</a>
+ {% else %}
+ <a href="#" class="btn disabled">{% trans "Edit" %}</a>
+ {% endif %}
+ </div>
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "orders/list.html" %}
+ </div>
+ <div class="tab-pane" id="tab3">
+ {% include "devices/list.html" with devices=customer.devices.all tag_id=None %}
+ </div>
+ <div class="tab-pane" id="tab4">
+ <table class="table">
+ <tbody>
+ {% for i in customer.note_set.all %}
+ <tr>
+ <td><a href="{% url 'notes-view_note' 'sent' i.pk %}">{{ i.code }}</a></td>
+ <td>{{ i.subject }}</td>
+ <td>{{ i.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ i.sender }}</td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="4" class="empty muted">{% trans "No messages found" %}</td></tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+</div>
+{% endblock third_column %}
+
+{% block crumbs %}
+ <li><a href="{% url 'customers-list_all' %}">{% trans "Customers" %}</a> <span class="divider">/</span></li>
+ <li class="active">{{ customer.name }}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/default.html b/servo/templates/default.html
new file mode 100755
index 0000000..922d567
--- /dev/null
+++ b/servo/templates/default.html
@@ -0,0 +1,185 @@
+{% load servo_tags %}
+{% load humanize %}
+{% load cache %}
+{% load i18n %}
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="icon" type="image/png" href="{{ STATIC_URL }}images/favicon.png">
+ <link rel="apple-touch-icon" type="image/png" href="{{ STATIC_URL }}images/apple-touch-icon.png">
+ <link href="{{ STATIC_URL }}js/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}js/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}css/bootstrap-datetimepicker.min.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}css/bootstrap-sortable.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}css/glyphicons.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}css/halflings.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}css/servo.css" rel="stylesheet">
+ <title>{{ title }} | Servo</title>
+</head>
+<body>
+ <div class="wrapper">
+ <div class="modal hide" id="modal"></div>
+ <div class="navbar navbar-fixed-top navbar-inverse">
+ <div class="navbar-inner">
+ <div class="container-fluid">
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <div class="nav-collapse collapse">
+ <ul class="nav">
+ <li class="{% active request request.user.username %}"><a href="{% url 'accounts-list_orders' request.user.username %}" title="{% trans "Homepage" %}"><i class="icon-home icon-white"></i></a></li>
+ <li class="{% active request '^orders' %}"><a href="{% url 'orders-index' %}?page=1"><i class="icon-inbox icon-white"></i> {% trans "Orders" %}</a></li>
+ <li class="{% active request '^sales' %}"><a href="{% url 'products-list_products' %}"><i class="icon-shopping-cart icon-white"></i> {% trans "Inventory" %}</a></li>
+ <li class="{% active request '^devices' %}"><a href="{% url 'devices-list' %}"><i class="icon-hdd icon-white"></i> {% trans "Devices" %}</a></li>
+ <li class="{% active request '^customers' %}"><a href="{% url 'customers-list_all' %}"><i class="icon-book icon-white"></i> {% trans 'Customers' %}</a></li>
+ <li class="{% active request '^notes' %}"><a href="{% url 'notes-list_notes' 'inbox' %}"><i class="icon-comment icon-white"></i> {% trans "Messages" %}</a></li>
+ <li class="{% active request '^stats' %}"><a href="{% url 'stats-index' %}"><i class="halflings stats white"></i> {% trans "Statistics" %}</a></li>
+ </ul>
+ <div class="nav-collapse collapse">
+ <form class="navbar-search pull-right" method="get" action="{{ request|search_url }}">
+ <input type="text" class="search-query" name="q" placeholder="{% trans "Search" %}" autocomplete="off" value="{{ request.session.search_query }}" id="toolbar-search" autofocus/>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="navbar navbar-fixed-top" style="top:41px;z-index:3">
+ <div class="navbar-inner">
+ <div class="container-fluid">
+ <div class="pull-left">
+ {% block toolbar %}
+ {% endblock toolbar %}
+ </div>
+ <div class="pull-right">
+ <ul class="nav pull-right">
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ <i class="icon-user"></i> <b class="caret"></b>
+ </a>
+ {% with request.user as user %}
+ <ul class="dropdown-menu">
+ <li><a href="#"><i class="icon-user"></i> {{ request.user.get_name }}</a></li>
+ <li class="divider"></li>
+ <li><a href="{% url 'accounts-list_orders' user.username %}"><i class="icon-home"></i> {% trans "Homepage" %}</a></li>
+ <li><a href="{% url 'calendars-list' user.username %}"><i class="icon-calendar"></i> {% trans "Calendars" %}</a></li>
+ <li><a href="{% url 'accounts-settings' user.username %}"><i class="icon-wrench"></i> {% trans "Profile" %}</a></li>
+ {% if request.user.is_staff %}
+ <li><a href="{% url 'admin-settings' %}"><i class="icon-cog"></i> {% trans "System Settings" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a href="{% url 'checkin-index' %}"><i class="icon-check"></i> {% trans "Go to check-in" %}</a></li>
+ <li><a href="{% url 'accounts-logout' %}" data-modal="#modal"><i class="icon-off"></i> {% trans "Log out" %}...</a></li>
+ </ul>
+ {% endwith %}
+ </li>
+ </ul>
+ {% with request.user.notifications as notifications %}
+ <ul class="nav pull-right">
+ {% with notifications|unread_notifications as alerts %}
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ <i class="icon-bell"></i> <span class="badge event-counter">{{ alerts|count_or_empty }}</span> <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu dropdown-messages">
+ {% for i in alerts|slice:":10" %}
+ <li class="{{ i.get_class }}">
+ <a href="{% url 'events-ack_event' i.pk %}" class="alt" data-rel=".event-counter">
+ <div>
+ <i class="icon-tasks"></i> {{ i.description }}
+ <br/>
+ <small class="muted">{{ i.triggered_at|naturaltime }}</small>
+ </div>
+ </a>
+ </li>
+ {% if not forloop.last %}
+ <li class="divider"></li>
+ {% endif %}
+ {% empty %}
+ <li class="disabled"><a href="#" class="text-center">{% trans "No new alerts" %}</a></li>
+ {% endfor %}
+ <li class="divider"></li>
+ <li>
+ <a class="text-center" href="{% url 'accounts-updates' request.user.username %}?kind=set_status">
+ <strong>{% trans "See All Alerts" %}</strong> <i class="icon-chevron-right"></i>
+ </a>
+ </li>
+ </ul>
+ </li>
+ {% endwith %}
+ </ul>
+ <ul class="nav pull-right">
+ <li class="dropdown">
+ {% with notifications|unread_messages as messages %}
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ <i class="icon-envelope"></i> <span class="badge msg-counter">{{ messages|count_or_empty }}</span> <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu dropdown-messages">
+ {% for i in messages|slice:":10" %}
+ <li class="{{ i.get_class }}">
+ <a href="{% url 'events-ack_event' i.pk %}" class="alt" data-rel=".msg-counter">
+ <div class="clearfix">
+ <strong class="pull-left">{{ i.triggered_by }}</strong>
+ <small class="pull-right muted">{{ i.triggered_at|naturaltime }}</small>
+ </div>
+ <div style="overflow:hidden">{{ i.description }}</div>
+ </a>
+ </li>
+ {% if not forloop.last %}
+ <li class="divider"></li>
+ {% endif %}
+ {% empty %}
+ <li class="disabled"><a href="#" class="text-center">{% trans "No new messages" %}</a></li>
+ {% endfor %}
+ {% endwith %}
+ <li class="divider"></li>
+ <li>
+ <a class="text-center" href="{% url 'accounts-updates' request.user.username %}?kind=note_added">
+ <strong>{% trans "Read All Messages" %}</strong> <i class="icon-chevron-right"></i>
+ </a>
+ </li>
+ </ul>
+ {% endwith %}
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="container-fluid container-main">
+ {% for message in messages %}
+ <div class="alert alert-block alert-{{ message.tags }}">
+ <button type="button" class="close" data-dismiss="alert">&times;</button>
+ <i class="icon-exclamation-sign"></i> <strong>{{ message }}</strong>
+ </div>
+ {% endfor %}
+ {% block content %}
+ {% endblock content %}
+ </div>
+ <div class="push"></div>
+ </div>
+ <div class="footer">
+ <div class="container-fluid">
+ <ul class="breadcrumb pull-left">
+ <li><i class="icon-home"></i> <a href="{% url 'accounts-list_orders' request.user.username %}">{{ request.user.get_full_name }}</a> <span class="divider">/</span></li>
+ {% block crumbs %}{% endblock crumbs %}
+ </ul>
+ <small class="copyright pull-right muted">&copy; {% now "Y" %} First Party Software | <a href="https://docs.servoapp.com/terms/" target="_blank">{% trans "Terms" %}</a></small>
+ </div>
+ </div>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/jquery.min.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/bootstrap-sortable.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/spin.min.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/jquery.spin.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/bootstrap-datetimepicker.min.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/common.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/modernizr.js"></script>
+ <script type="text/javascript" src="{{ STATIC_URL }}js/servo.js"></script>
+ {% block media %}{% endblock media %}
+</body>
+</html>
diff --git a/servo/templates/default_print.html b/servo/templates/default_print.html
new file mode 100755
index 0000000..ed3f4de
--- /dev/null
+++ b/servo/templates/default_print.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>{{ title }}</title>
+ <meta charset="utf-8">
+ <link rel="icon" type="image/png" href="{{ STATIC_URL }}images/favicon.png">
+ <link href="{{ STATIC_URL }}js/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+</head>
+<body>
+ <div class="container">
+ {% block content %}
+ {% endblock content %}
+ </div>
+ <script src="{{ STATIC_URL }}js/jquery.min.js"></script>
+ {% if request.user.autoprint %}
+ <script type="text/javascript">
+ $(function() {
+ window.print();
+ });
+ </script>
+ {% endif %}
+</body>
+</html>
diff --git a/servo/templates/devices/accessories_edit.html b/servo/templates/devices/accessories_edit.html
new file mode 100755
index 0000000..db390b3
--- /dev/null
+++ b/servo/templates/devices/accessories_edit.html
@@ -0,0 +1,40 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}{% trans "Manage Accessories" %}{% endblock header %}
+
+{% block body %}
+<form method="post" action="{{ action }}" class="form-search" id="acc-form">
+ {% csrf_token %}
+ <input type="text" id="filter-acc" autocomplete="off" name="name" style="width:500px"/>
+ <div id="acc-choices" style="margin-top:10px">
+ {% for a in selected %}
+ <div class="row-fluid">
+ <div class="span10">{{ a.name }}</div>
+ <div class="span1">
+ <a href="{{ action }}{{ a.id|safe }}/delete/" class="btn btn-small delete">{% trans "Remove" %}</a>
+ </div>
+ </div>
+ {% empty %}
+ <p class="empty muted">{% trans "No accessories added" %}</p>
+ {% endfor %}
+ </div>
+</form>
+<script type="text/javascript">
+ $(function() {
+ $('#acc-choices').on('click', 'a.delete', function() {
+ $('#acc-choices').load($(this).attr('href') + ' #acc-choices');
+ return false;
+ });
+ $('#filter-acc').typeahead({source: {{ choices_json }}});
+ $('#acc-form').submit(function(e) {
+ e.preventDefault();
+ url = $(this).attr('action');
+ $('#acc-choices').load(url + ' #acc-choices', $(this).serializeArray(),
+ function() {
+ $('#filter-acc').val('');
+ });
+ });
+ });
+</script>
+{% endblock body %}
diff --git a/servo/templates/devices/choose-error.html b/servo/templates/devices/choose-error.html
new file mode 100755
index 0000000..73da7e2
--- /dev/null
+++ b/servo/templates/devices/choose-error.html
@@ -0,0 +1,5 @@
+{% load i18n %}
+{% load servo_tags %}
+<ul class="nav nav-pills nav-stacked">
+ <li class="text-center muted">{{ error }}</li>
+</ul>
diff --git a/servo/templates/devices/choose-list.html b/servo/templates/devices/choose-list.html
new file mode 100755
index 0000000..8f5d3f4
--- /dev/null
+++ b/servo/templates/devices/choose-list.html
@@ -0,0 +1,12 @@
+{% load i18n %}
+<ul class="nav nav-list">
+{% for i in results %}
+{% if i.pk %}
+ <li><a href="{% url 'orders-add_device' pk=order device_id=i.pk %}"><strong>{{ i.description }}</strong><br/><small>{{ i.configuration }}</small></a></li>
+{% else %}
+ <li><a href="{% url 'orders-add_device' pk=order sn=i.sn %}"><strong>{{ i.description }}</strong><br/><small>{{ i.configuration }}</small></a></li>
+{% endif %}
+{% empty %}
+ <li style="text-align:center">{% trans "No search results" %}</li>
+{% endfor %}
+</ul>
diff --git a/servo/templates/devices/choose.html b/servo/templates/devices/choose.html
new file mode 100755
index 0000000..49d7151
--- /dev/null
+++ b/servo/templates/devices/choose.html
@@ -0,0 +1,26 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Search for device" %}
+{% endblock header %}
+
+{% block body %}
+<form method="post" action="{% url 'devices-choose' order %}" accept-charset="utf-8" data-target="#search-results" id="search-form">
+ {% csrf_token %}
+ <input type="text" style="width:500px" class="search-query" name="q" autocomplete="off" placeholder="{% trans "Serial number or IMEI code" %}" id="device-sn"/>
+</form>
+<div id="search-results"></div>
+{% endblock body %}
+
+{% block footer %}
+<a id="add-device" href="{% url 'devices-add' %}" class="btn">{% trans "New Device" %}</a>
+<a class="btn btn-primary trigger-search" href="#">{% trans "Search" %}</a>
+<script type="text/javascript">
+ $('#add-device').on('click', function() {
+ var url = $(this).attr('href') + '?sn=' + $('#device-sn').val();
+ document.location = url;
+ return false;
+ });
+</script>
+{% endblock footer %}
diff --git a/servo/templates/devices/diagnostic_error.html b/servo/templates/devices/diagnostic_error.html
new file mode 100644
index 0000000..242b340
--- /dev/null
+++ b/servo/templates/devices/diagnostic_error.html
@@ -0,0 +1 @@
+<div class="muted text-center">{{ error }}</div>
diff --git a/servo/templates/devices/diagnostic_init.html b/servo/templates/devices/diagnostic_init.html
new file mode 100644
index 0000000..6363ca9
--- /dev/null
+++ b/servo/templates/devices/diagnostic_init.html
@@ -0,0 +1,16 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}{% trans "Initiate iOS diagnostics" %}{% endblock header %}
+
+{% block body %}
+ <form action="{{ url }}?a=init" method="post" class="form-horizontal">{% csrf_token %}
+ <input type="hidden" name="order" value="{{ order.pk|safe }}"/>
+ <div class="control-group">
+ <label class="control-label" for="id_email">{% trans "Email" %}</label>
+ <div class="controls">
+ <input id="id_email" maxlength="128" name="email" type="email" value="{{ customer.email }}"/>
+ </div>
+ </div>
+ </form>
+{% endblock body %}
diff --git a/servo/templates/devices/diagnostic_ios.html b/servo/templates/devices/diagnostic_ios.html
new file mode 100644
index 0000000..7dee528
--- /dev/null
+++ b/servo/templates/devices/diagnostic_ios.html
@@ -0,0 +1,20 @@
+<dl class="dl-horizontal">
+{% for k, v in diagnostics.diags.result.items %}
+ <dt>{{ k }}</dt>
+ <dd>{{ v }}</dd>
+{% endfor %}
+</dl>
+
+<dl class="dl-horizontal">
+{% for k, v in diagnostics.diags.profile.items %}
+ <dt>{{ k }}</dt>
+ <dd>{{ v }}</dd>
+{% endfor %}
+</dl>
+
+<dl class="dl-horizontal">
+{% for k, v in diagnostics.diags.report.items %}
+ <dt>{{ k }}</dt>
+ <dd>{{ v }}</dd>
+{% endfor %}
+</dl>
diff --git a/servo/templates/devices/diagnostic_results.html b/servo/templates/devices/diagnostic_results.html
new file mode 100755
index 0000000..85b9748
--- /dev/null
+++ b/servo/templates/devices/diagnostic_results.html
@@ -0,0 +1,22 @@
+{% load i18n %}
+<dl class="dl-horizontal">
+{% with diagnostics.eventHeader as header %}
+ <dt>{% trans "Result" %}</dt>
+ <dd>{{ header.diagnosticEventEndResult|default:"-" }}</dd>
+ <dt>{% trans "Tool" %}</dt>
+ <dd>{{ header.toolID|default:"-" }} ({{ header.toolVersion|default:"-" }})</dd>
+ <dt>{% trans "Passed modules" %}</dt>
+ <dd>{{ header.modulePassCount|default:"-" }}</dd>
+{% endwith %}
+</dl>
+
+<table class="table">
+{% for i in diagnostics.eventItems %}
+ <tr>
+ <td>{{ i.moduleName }}</td>
+ <td>{{ i.moduleLocation }}</td>
+ <td>{{ i.moduleTestName }}</td>
+ <td>{{ i.moduleTestResult }}</td>
+ </tr>
+{% endfor %}
+</table>
diff --git a/servo/templates/devices/diagnostics.html b/servo/templates/devices/diagnostics.html
new file mode 100755
index 0000000..eb146c0
--- /dev/null
+++ b/servo/templates/devices/diagnostics.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}{% trans "Diagnostics" %}{% endblock header %}
+
+{% block body %}
+ <div id="gsx-container" data-source="{% url 'devices-diagnostics' pk=device.id %}?a=get">
+ <div class="progress progress-striped active">
+ <div class="bar" style="width:100%;" data-progress="0"></div>
+ </div>
+ <div class="text-center muted">{% trans "Fetching diagnostics..." %}</div>
+ </div>
+{% endblock body %}
diff --git a/servo/templates/devices/find.html b/servo/templates/devices/find.html
new file mode 100755
index 0000000..1f8e291
--- /dev/null
+++ b/servo/templates/devices/find.html
@@ -0,0 +1,66 @@
+{% extends "devices/index.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<ul class="nav nav-tabs" style="margin-top:30px">
+ <li><a href="{% url 'devices-list' %}">{% trans "Browse" %}</a></li>
+ <li class="active"><a href="{% url 'devices-find' %}">{% trans "Search" %}</a></li>
+</ul>
+
+<form action="" method="post" class="form-inline well clearfix">
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <p class="clearfix">
+ <hr/>
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </p>
+</form>
+
+<table class="table table-striped sortable">
+ <thead>
+ <th>{% trans "Serial Number" %}</th>
+ <th>{% trans "Description" %}</th>
+ <th>{% trans "Created" %}</th>
+ <th>{% trans "Warranty Status" %}</th>
+ <th data-defaultsort="disabled"></th>
+ </thead>
+ <tbody>
+ {% for i in devices %}
+ <tr>
+ <td>{{ i.sn|safe }}</td>
+ <td>{{ i.description }}</td>
+ <td>{{ i.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ i.get_warranty_status_display }}</td>
+ <td>
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if request.session.current_order_id %}
+ <li><a href="{% url 'orders-add_device' request.session.current_order_id i.id %}">{% trans "Use in" %} #{{ request.session.current_order_code }}</a></li>
+ <li class="divider"></li>
+ {% endif %}
+ <li><a href="{% url 'orders-create_with_sn' sn=i.sn|safe %}">{% trans "Create Service Order" %}</a></li>
+ <li class="divider"></li>
+ <li><a href="{{ i.get_absolute_url }}">{% trans "View" %}</a></li>
+ <li><a href="{{ i.get_absolute_url }}edit/">{% trans "Edit" %}</a></li>
+ <li class="divider"></li>
+ <li><a href="{{ i.get_absolute_url }}delete/" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="5" class="empty muted">{% trans "No search results" %}</td></tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% include "pagination.html" with items=devices %}
+{% endblock content %}
diff --git a/servo/templates/devices/form.html b/servo/templates/devices/form.html
new file mode 100755
index 0000000..8682101
--- /dev/null
+++ b/servo/templates/devices/form.html
@@ -0,0 +1,44 @@
+{% extends "devices/view.html" %}
+{% load i18n %}
+
+{% block fourth_column %}
+<form method="post" action="" accept-charset="utf-8" class="form-horizontal" enctype="multipart/form-data">
+ {% csrf_token %}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Details" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.description %}
+ {% include "form_field_snippet.html" with field=form.sn %}
+ {% include "form_field_snippet.html" with field=form.warranty_status %}
+ {% include "form_field_snippet.html" with field=form.username %}
+ {% include "form_field_snippet.html" with field=form.password %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "form_field_snippet.html" with field=form.purchased_on %}
+ {% include "form_field_snippet.html" with field=form.brand %}
+ {% include "form_field_snippet.html" with field=form.reseller %}
+ {% include "form_field_snippet.html" with field=form.product_line %}
+ {% include "form_field_snippet.html" with field=form.configuration %}
+ <div class="control-group">
+ <label class="control-label">{% trans "Tags" %}</label>
+ <div class="controls">
+ <input type="text" name="tag" class="input typeahead" placeholder="{% trans "Enter tag" %}" data-provide="typeahead" data-source="{% url 'api-tags' %}?type=device" autocomplete="off"/>
+ <div class="clearfix" style="margin-top:10px">
+ {% for t in device.tags.all %}
+ <span class="label"><div class="pull-left">{{ t.tag }}</div><a class="close" href="{% url 'tags-clear' t.pk %}"><i class="icon-remove icon-white"></i></a></span>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ {% include "form_field_snippet.html" with field=form.photo %}
+ {% include "form_field_snippet.html" with field=form.notes %}
+ {% include "form_field_snippet.html" with field=form.has_onsite %}
+ {% include "form_field_snippet.html" with field=form.is_vintage %}
+ </div>
+ </div>
+ {% include "form_buttons.html" %}
+</form>
+{% endblock fourth_column %}
diff --git a/servo/templates/devices/get_info.html b/servo/templates/devices/get_info.html
new file mode 100755
index 0000000..e16b1c5
--- /dev/null
+++ b/servo/templates/devices/get_info.html
@@ -0,0 +1,45 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{{ device.description }}
+{% endblock header %}
+
+{% block body %}
+<dl class="dl-horizontal">
+ <dt>{% trans "Warranty Status" %}</dt>
+ <dd>{{ device.get_warranty_status_display }}</dd>
+ <dt>{% trans "Purchase Date" %}</dt>
+ <dd>{{ device.purchased_on|date:"SHORT_DATE_FORMAT"|default:"-" }}, {{ device.purchase_country|default:"-" }}</dd>
+ <dt>{% trans "Serial Number" %}</dt>
+ <dd>{{ device.sn }}</dd>
+ {% if device.configuration %}
+ <dt>{% trans "Configration" %}</dt>
+ <dd>{{ device.configuration }}</dd>
+ {% endif %}
+ {% if device.imei %}
+ <dt>IMEI</dt>
+ <dd>{{ device.imei }}</dd>
+ <dt>{% trans "Activation Profile" %}</dt>
+ <dd>{{ device.initial_activation_policy }}</dd>
+ <dt>{% trans "Unlocked" %}</dt>
+ <dd>{{ device.unlocked|yesno }}</dd>
+ {% endif %}
+ {% if device.username %}
+ <dt>{% trans "Username" %}</dt>
+ <dd>{{ device.username }}</dd>
+ {% endif %}
+ {% if device.password %}
+ <dt>{% trans "Password" %}</dt>
+ <dd>{{ device.password }}</dd>
+ {% endif %}
+ {% if device.get_coverage_details %}
+ <dt>{% trans "Details" %}</dt>
+ <dd>{{ device.get_coverage_details|join:"<br/>" }}</dd>
+ {% endif %}
+ {% if device.notes %}
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ device.notes }}</dd>
+ {% endif %}
+</dl>
+{% endblock body %}
diff --git a/servo/templates/devices/index.html b/servo/templates/devices/index.html
new file mode 100755
index 0000000..aba62b1
--- /dev/null
+++ b/servo/templates/devices/index.html
@@ -0,0 +1,78 @@
+{% extends "default.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+{% if model %}
+<a href="{% url 'devices-create_device' product_line=product_line model=model %}" class="btn"><i class="icon-plus"></i> {% trans "New Device" %}</a>
+<a href="{% url 'devices-model_parts' product_line=product_line model=model %}" class="btn"><i class="icon-wrench"></i> {% trans "Show Parts" %}</a>
+{% else %}
+<a href="#" class="btn disabled"><i class="icon-plus"></i> {% trans "New Device" %}</a>
+<a href="#" class="btn disabled"><i class="icon-wrench"></i> {% trans "Show Parts" %}</a>
+{% endif %}
+<a href="{% url 'devices-upload_devices' %}" class="btn" data-modal="#modal"><i class="icon-upload"></i> {% trans "Upload" %}</a>
+{% endblock toolbar %}
+
+{% block content %}
+
+<ul class="nav nav-tabs" style="margin-top:30px">
+ <li class="active"><a href="{% url 'devices-list' %}">{% trans "Browse" %}</a></li>
+ <li><a href="{% url 'devices-find' %}">{% trans "Search" %}</a></li>
+</ul>
+
+<div class="row-fluid">
+ <div class="span2">
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Product Line" %}</li>
+ {% for k, v in product_lines.items %}
+ <li {% if k == product_line %}class="active"{% endif %}><a href="{% url 'devices-list_devices' product_line=k %}">{{ v.name }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+
+ <div class="span2">
+ {% block second_column %}
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Model" %}</li>
+ {% for k, v in models.items %}
+ <li {% if k == model %}class="active"{% endif %}><a href="{% url 'devices-list_devices' product_line=product_line model=k %}">{{ v }}</a></li>
+ {% empty %}
+ <li class="disabled"><a href="">{% trans "No Product Line selected" %}</a></li>
+ {% endfor %}
+ </ul>
+ {% endblock second_column %}
+ </div>
+
+ <div class="span2">
+ {% block third_column %}
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Device" %}</li>
+ {% for i in devices %}
+ <li {% if i.pk == device.pk %}class="active"{% endif %}>
+ <a href="{% url 'devices-view_device' product_line=product_line model=model pk=i.pk %}">
+ <strong>{{ i.sn }}</strong><br/>
+ <small>{{ i.get_warranty_status_display|truncatechars:30 }}</small>
+ </a>
+ </li>
+ {% empty %}
+ <li class="disabled"><a href="">{% trans "No devices found" %}</a></li>
+ {% endfor %}
+ </ul>
+ {% endblock third_column %}
+ </div>
+ <div class="span6">
+ {% block fourth_column %}
+ {% if query %}
+ {% include "devices/parts.html" %}
+ {% else %}
+ <h2 class="muted text-center">{% trans "No device selected" %}</h2>
+ {% endif %}
+ {% endblock fourth_column %}
+ </div>
+</div>
+
+{% endblock content %}
+
+{% block crumbs %}
+ <li><a href="{% url 'devices-list' %}">{% trans "Devices" %}</a></li>{% block path %}{% endblock path %}
+{% endblock crumbs %}
diff --git a/servo/templates/devices/list.html b/servo/templates/devices/list.html
new file mode 100755
index 0000000..b11cab8
--- /dev/null
+++ b/servo/templates/devices/list.html
@@ -0,0 +1,38 @@
+{% load i18n %}
+
+<table class="table">
+ <tbody class="searchable">
+ {% for device in devices %}
+ <tr>
+ <td><a href="{{ device.get_absolute_url }}">{{ device.sn }}</a></td>
+ <td>{{ device.description }}</td>
+ <td>{{ device.purchased_on|default:"-" }}</td>
+ <td>{{ device.get_warranty_status_display }}</td>
+ <td>
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% with request.session.current_order_id as co %}
+ {% if co %}
+ <li><a href="{% url 'orders-add_device' co device.pk %}">{% trans "Use in" %} #{{ request.session.current_order_code }}</a></li>
+ <li class="divider"></li>
+ {% endif %}
+ {% endwith %}
+ <li><a href="{% url 'orders-create_with_device' device_id=device.pk %}">{% trans "Create Service Order" %}</a></li>
+ <li {% if device.order_set.count < 1 %}class="disabled"{% endif %}><a href="{% url 'orders-index' %}?device={{ device.pk|safe }}">{% trans "Show Service Orders" %}</a></li>
+ <li class="divider"></li>
+ <li><a href="{{ device.get_absolute_url }}">{% trans "View" %}</a></li>
+ <li><a href="{{ device.get_absolute_url }}edit/">{% trans "Edit" %}</a></li>
+ <li class="divider"></li>
+ <li><a href="{{ device.get_absolute_url }}delete/" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="5" class="empty muted">{% trans "No devices found" %}</td></tr>
+ {% endfor %}
+ </tbody>
+</table>
diff --git a/servo/templates/devices/parts.html b/servo/templates/devices/parts.html
new file mode 100755
index 0000000..1f4f12c
--- /dev/null
+++ b/servo/templates/devices/parts.html
@@ -0,0 +1,42 @@
+{% load i18n %}
+{% load servo_tags %}
+<table class="table table-hover">
+ <thead>
+ <tr>
+ {% if order.is_editable %}
+ <th style="width:20px"></th>
+ {% endif %}
+ <th>{% trans "Part" %}</th>
+ <th>{% trans "Stock Price" %}</th>
+ <th>{% trans "Exchange Price" %}</th>
+ </tr>
+ </thead>
+ <tbody class="searchable">
+ {% for p in products %}
+ <tr>
+ {% if order.is_editable %}
+ <td><input type="checkbox" data-url="{% url 'orders-add_part' pk=order.pk device=device.pk code=p.code %}" class="async" data-reload="#products"/></td>
+ {% endif %}
+ <td>
+ <a href="{% url 'products-get_info' p.code request.user.get_location.pk %}" data-modal="#modal"><strong>{{ p.code }}</strong></a>{% if p.subst_code %}<br/><small class="muted">{% blocktrans with code=p.subst_code %}Substituted to {{ code }}{% endblocktrans %}</small>{% endif %}<br/>
+ {{ p.title }}<br/><small class="muted">{{ p.eee_code|default:"-"|addspace }}</small></td>
+ <td>{{ p.price_sales_stock|currency }}</td>
+ <td>{{ p.price_sales_exchange|currency }}</td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="5" class="muted empty">{% trans "No products found" %}</td></tr>
+ {% endfor %}
+ </tbody>
+ </table>
+
+ <script type="text/javascript">
+ $('input.async').click(function(){
+ var that = $(this);
+ var url = $(this).data('url');
+ var reload = $(this).data('reload');
+ $.get(url, function(r) {
+ $(reload).html(r);
+ $(that).attr('disabled', 'disabled');
+ });
+ });
+ </script>
diff --git a/servo/templates/devices/remove.html b/servo/templates/devices/remove.html
new file mode 100755
index 0000000..8bd7def
--- /dev/null
+++ b/servo/templates/devices/remove.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with sn=device.sn%}Delete device {{ sn }}"?{% endblocktrans %}
+{% endblock header %}
+
+{% block body %}
+ {% trans "This will also remove it from all service orders." %}
+{% endblock body %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/devices/search.html b/servo/templates/devices/search.html
new file mode 100755
index 0000000..28760b5
--- /dev/null
+++ b/servo/templates/devices/search.html
@@ -0,0 +1,15 @@
+{% extends "two_column_layout.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block first_column %}
+<input type="text" class="input-large search-query filter" placeholder="{% trans "Filter results" %}"/>
+{% endblock first_column %}
+
+{% block second_column %}
+{% include "devices/list.html" %}
+{% endblock second_column %}
+
+{% block crumbs %}
+<li><a href="{% url 'devices-list' %}">{% trans "Devices" %}</a></li><li class="active"><span class="divider">/</span>{{ title }}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/devices/search_gsx.html b/servo/templates/devices/search_gsx.html
new file mode 100755
index 0000000..4f06b81
--- /dev/null
+++ b/servo/templates/devices/search_gsx.html
@@ -0,0 +1,41 @@
+{% extends "devices/search.html" %}
+{% load i18n %}
+
+{% block second_column %}
+{% block tabs %}
+ <ul class="nav nav-tabs" id="gsx-tabs">
+ {% if param == 'serialNumber' or param == 'alternateDeviceId' %}
+ <li><a href="{% url 'devices-search_gsx' 'warranty' param query %}">{% trans "Device" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Device" %}</a></li>
+ {% endif %}
+ {% if param == 'serialNumber' or param == 'alternateDeviceId' %}
+ <li><a href="{% url 'devices-search_gsx' 'orders' param query %}">{% trans "Orders" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Orders" %}</a></li>
+ {% endif %}
+ {% if param == 'serialNumber' or param == 'partNumber' %}
+ <li><a href="{% url 'devices-search_gsx' 'parts' param query %}">{% trans "Parts" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Parts" %}</a></li>
+ {% endif %}
+ {% if param == 'serialNumber' or param == 'dispatchId' %}
+ <li><a href="{% url 'devices-search_gsx' 'repairs' param query %}">{% trans "Repairs" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Repairs" %}</a></li>
+ {% endif %}
+ </ul>
+{% endblock tabs %}
+
+{% block results %}
+ {% include "devices/search_gsx_results.html" %}
+{% endblock results %}
+
+{% endblock second_column %}
+
+{% block media %}
+<script type="text/javascript">
+ var loc = location.pathname + location.search;
+ $('#gsx-tabs>li>a[href="'+loc+'"]').parent().addClass('active');
+</script>
+{% endblock media %}
diff --git a/servo/templates/devices/search_gsx_error.html b/servo/templates/devices/search_gsx_error.html
new file mode 100755
index 0000000..1a0de98
--- /dev/null
+++ b/servo/templates/devices/search_gsx_error.html
@@ -0,0 +1,19 @@
+{% extends "devices/search.html" %}
+{% load i18n %}
+
+{% block second_column %}
+{% block tabs %}
+ <ul class="nav nav-tabs" id="gsx-tabs">
+ {% if param == 'serialNumber' or 'alternateDeviceId' %}
+ <li class="active"><a href="#">{% trans "Device" %}</a></li>
+ {% endif %}
+ <li class="disabled"><a href="#">{% trans "Parts" %}</a></li>
+ <li class="disabled"><a href="#">{% trans "Repairs" %}</a></li>
+ </ul>
+{% endblock tabs %}
+
+{% block results %}
+ {% include "search/results/gsx_error.html" %}
+{% endblock results %}
+
+{% endblock second_column %}
diff --git a/servo/templates/devices/search_gsx_parts.html b/servo/templates/devices/search_gsx_parts.html
new file mode 100755
index 0000000..63bfdb9
--- /dev/null
+++ b/servo/templates/devices/search_gsx_parts.html
@@ -0,0 +1,30 @@
+{% load i18n %}
+{% load servo_tags %}
+
+{% autoescape on %}
+{% for p in results %}
+ <tr>
+ <td><a href="{% url 'products-view_product' code=p.code %}">{{ p.code }}</a></td>
+ <td>{{ p.title }}<br/><span class="muted small" title="{{ p.eee_code }}">{{ p.eee_code|truncatechars:40 }}</td>
+ <td>{{ p.price_sales_exchange|currency }}</td>
+ <td>{{ p.price_sales_stock|currency }}</td>
+ <td>0</td>
+ <td>
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li{% if not perms.servo.add_product %} class="disabled"{% endif %}>
+ {% if device %}
+ <a href="{% url 'products-create' group=device.get_product_category code=p.code %}">{% trans "Create Product" %}</a>
+ {% else %}
+ <a href="{% url 'products-create' code=p.code %}">{% trans "Create Product" %}</a>
+ {% endif %}
+ </li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+{% endfor %}
+{% endautoescape %}
diff --git a/servo/templates/devices/search_gsx_repairs.html b/servo/templates/devices/search_gsx_repairs.html
new file mode 100755
index 0000000..6cb9f7f
--- /dev/null
+++ b/servo/templates/devices/search_gsx_repairs.html
@@ -0,0 +1,27 @@
+{% load i18n %}
+<table class="table table-hover">
+ <thead>
+ <tr>
+ <th>{% trans "Number" %}</th>
+ <th>{% trans "Reference" %}</th>
+ <th>{% trans "Created" %}</th>
+ <th>{% trans "Customer" %}</th>
+ <th>{% trans "Status" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for r in results %}
+ <tr>
+ {% with r.repairConfirmationNumber as repconf %}
+ <td><a href="{% url 'repairs-get_details' repconf %}" data-modal="#modal">{{ repconf }}</a></td>
+ {% endwith %}
+ <td>{{ r.purchaseOrderNumber|safe }}</td>
+ <td>{{ r.createdOn }}</td>
+ <td>{{ r.customerName }}</td>
+ <td>{{ r.repairStatus }}</td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="5" class="muted text-center">{% trans "No search results" %}</td></tr>
+ {% endfor %}
+ </tbody>
+</table>
diff --git a/servo/templates/devices/search_gsx_results.html b/servo/templates/devices/search_gsx_results.html
new file mode 100755
index 0000000..b7a0926
--- /dev/null
+++ b/servo/templates/devices/search_gsx_results.html
@@ -0,0 +1,5 @@
+<div id="gsx-container" data-source="{% url 'devices-get_gsx_search_results' what param query %}">
+ <div class="progress active">
+ <div class="bar" style="width:1%;" data-progress="0"></div>
+ </div>
+</div>
diff --git a/servo/templates/devices/search_gsx_warranty.html b/servo/templates/devices/search_gsx_warranty.html
new file mode 100755
index 0000000..e278001
--- /dev/null
+++ b/servo/templates/devices/search_gsx_warranty.html
@@ -0,0 +1,71 @@
+{% load i18n %}
+
+{% for d in results %}
+<div class="row-fluid">
+ <div class="span3">
+ <img class="img-rounded" src="{{ d.get_image_url }}" alt="{{ d.description }}" title="{{ d.description }}"/>
+ </div>
+ <div class="span9" id="gsx-results">
+ <h3>{{ d.description }}</h3>
+ {% if d.tags %}
+ {% for t in d.tags.all %}<span class="label pull-right"><i class="icon icon-tag"></i>{{ t.tag }}</span>{% endfor %}
+ {% endif %}
+ <dl class="dl-horizontal">
+ <dt>{% trans "Warranty Status" %}</dt>
+ <dd>{{ d.get_warranty_status_display }}</dd>
+ <dt>{% trans "Purchase Date" %}</dt>
+ <dd>{{ d.purchased_on|date:"SHORT_DATE_FORMAT"|default:"-" }}, {{ d.purchase_country|default:"-" }}</dd>
+ <dt>{% trans "Serial Number" %}</dt>
+ <dd>{{ d.sn }}</dd>
+ <dt>{% trans "Configration" %}</dt>
+ <dd>{{ d.configuration }}</dd>
+ {% if d.imei %}
+ <dt>{% trans "IMEI" %}</dt>
+ <dd>{{ d.imei|safe }}</dd>
+ <dt>{% trans "Applied Activation Policy" %}</dt>
+ <dd>{{ d.applied_activation_policy }}</dd>
+ <dt>{% trans "Initial Activation Policy" %}</dt>
+ <dd>{{ d.initial_activation_policy }}</dd>
+ <dt>{% trans "Next Tether Policy" %}</dt>
+ <dd>{{ d.next_tether_policy }}</dd>
+ <dt>{% trans "Find My iPhone" %}</dt>
+ <dd>{{ d.get_fmip_status }}</dd>
+ <dt>{% trans "Unlocked" %}</dt>
+ <dd>{{ d.unlocked|yesno }}</dd>
+ {% endif %}
+ {% if d.repeat_service %}
+ {% with d.repeat_service as order %}
+ <dt>{% trans "Sales Order" %}</dt>
+ <dd><a href="{{ order.get_absolute_url }}">{{ d.repeat_service.code }}</a> ({{ order.created_at|date:"SHORT_DATE_FORMAT" }})</dd>
+ {% endwith %}
+ {% endif %}
+ {% if d.get_coverage_details %}
+ <dt>{% trans "Details" %}</dt>
+ <dd>{{ d.get_coverage_details|join:"<br/>" }}</dd>
+ {% endif %}
+ {% if d.has_onsite %}
+ <dt>{% trans "Onsite Coverage" %}</dt>
+ <dd>{{ d.onsite_start_date }} - {{ d.onsite_end_date }}</dd>
+ {% endif %}
+ {% if d.contract_start_date %}
+ <dt>{% trans "Contract Coverage" %}</dt>
+ <dd>{{ d.contract_start_date }} - {{ d.contract_end_date }}</dd>
+ {% endif %}
+ {% if d.notes %}
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ d.notes }}</dd>
+ {% endif %}
+ </dl>
+ <hr/>
+ {% if request.session.current_order_id %}
+ {% with request.session.current_order_id as co %}
+ <a class="btn btn-primary" href="{% url 'orders-add_device' pk=co sn=d.sn %}"><i class="icon-share-alt icon-white"></i> {% trans "Use in order" %} #{{ request.session.current_order_code }}</a>
+ {% endwith %}
+ {% endif %}
+ <a class="btn btn-default" href="{% url 'orders-create_with_sn' sn=d.sn %}"><i class="icon-plus"></i> {% trans "Create Service Order" %}</a>
+ {% if d.manual_url %}
+ <a class="btn" href="{{ d.manual_url }}"><i class="icon-download"></i> {% trans "Download Manual" %}</a></li>
+ {% endif %}
+ </div>
+</div>
+{% endfor %}
diff --git a/servo/templates/devices/specs.html b/servo/templates/devices/specs.html
new file mode 100755
index 0000000..dcad375
--- /dev/null
+++ b/servo/templates/devices/specs.html
@@ -0,0 +1,42 @@
+{% extends "devices/index.html" %}
+{% load mptt_tags %}
+{% load i18n %}
+
+{% block navtabs %}
+<ul class="nav nav-tabs" id="navtabs">
+ <li><a href="/devices/">{% trans "All" %}</a></li>
+ <li><a href="/devices/specs/">{% trans "Specs" %}</a></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">{% trans "Tagi" %} <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ {% for t in tags %}
+ <li><a href="/customers/index/tag/{{ t.id }}/">{{ t.title }}</a></li>
+ {% endfor %}
+ </ul>
+ </li>
+ <li class="pull-right"><a href="/devices/specs/new/">{% trans "Luo malli" %}</a></li>
+</ul>
+{% endblock navtabs %}
+
+{% block content %}
+<div class="row">
+<div class="span3">
+ <ul>
+ {% recursetree specs %}
+ <li><a href="/devices/specs/{{ node.id }}/">{{ node.title }}</a>
+ {% if not node.is_leaf_node %}
+ <ul class="children">
+ {{ children }}
+ </ul>
+ {% endif %}
+ </li>
+ {% endrecursetree %}
+ </ul>
+ </div>
+
+ <div class="span9">
+ {% block specs %}
+ {% endblock specs %}
+ </div>
+</div>
+{% endblock content %}
diff --git a/servo/templates/devices/summary.html b/servo/templates/devices/summary.html
new file mode 100755
index 0000000..cc6b4dc
--- /dev/null
+++ b/servo/templates/devices/summary.html
@@ -0,0 +1,48 @@
+{% load i18n %}
+<div class="row-fluid">
+ <div class="span3">
+ <img src="{{ device.get_photo }}" alt="{{ device.description }}" title="{{ device.description }}" class="img-rounded"/>
+ </div>
+ <div class="span9">
+ <h3>{{ device.description }}</h3>
+ <dl class="dl-horizontal">
+ <dt>{% trans "Warranty Status" %}</dt>
+ <dd>{{ device.get_warranty_status_display }}</dd>
+ <dt>{% trans "Purchase Date" %}</dt>
+ <dd>{{ device.purchased_on|date:"SHORT_DATE_FORMAT"|default:"-" }}, {{ device.get_purchase_country|default:"-" }}</dd>
+ <dt>{% trans "Serial Number" %}</dt>
+ <dd>{{ device.sn|default:"-" }}</dd>
+ {% if device.configuration %}
+ <dt>{% trans "Configration" %}</dt>
+ <dd>{{ device.configuration }}</dd>
+ {% endif %}
+ {% if device.username %}
+ <dt>{% trans "Username" %}</dt>
+ <dd>{{ device.username }}</dd>
+ {% endif %}
+ {% if device.password %}
+ <dt>{% trans "Password" %}</dt>
+ <dd>{{ device.password }}</dd>
+ {% endif %}
+ {% if device.imei %}
+ <dt>{% trans "IMEI" %}</dt>
+ <dd>{{ device.imei }}</dd>
+ <dt>{% trans "Applied Activation Policy" %}</dt>
+ <dd>{{ device.applied_activation_policy }}</dd>
+ <dt>{% trans "Initial Activation Policy" %}</dt>
+ <dd>{{ device.initial_activation_policy }}</dd>
+ <dt>{% trans "Find My iPhone" %}</dt>
+ <dd>{{ device.get_fmip_status }}</dd>
+ <dt>{% trans "Unlocked" %}</dt>
+ <dd>{{ device.unlocked }}</dd>
+ {% endif %}
+ {% if device.notes %}
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ device.notes }}</dd>
+ {% endif %}
+ </dl>
+ {% for t in device.tags.all %}
+ <span class="label">{{ t.tag }}</span>
+ {% endfor %}
+ </div>
+</div>
diff --git a/servo/templates/devices/upload_devices.html b/servo/templates/devices/upload_devices.html
new file mode 100755
index 0000000..1d30242
--- /dev/null
+++ b/servo/templates/devices/upload_devices.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Upload devices" %}
+{% endblock header %}
+
+{% block body %}
+<form action="{{ action }}" method="post" enctype="multipart/form-data" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+</form>
+{% endblock body %}
diff --git a/servo/templates/devices/view.html b/servo/templates/devices/view.html
new file mode 100755
index 0000000..fd6d3a9
--- /dev/null
+++ b/servo/templates/devices/view.html
@@ -0,0 +1,83 @@
+{% extends "devices/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block fourth_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Device" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Orders" %}</a></li>
+ {% if device.is_apple_device %}
+ <li><a href="#tab3" data-toggle="tab">{% trans "GSX Repairs" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#" data-toggle="tab">{% trans "GSX Repairs" %}</a></li>
+ {% endif %}
+</ul>
+<div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "devices/summary.html" %}
+ <hr/>
+ {% if request.session.current_order_id and device.id %}
+ <a href="{% url 'orders-add_device' request.session.current_order_id device.id %}" class="btn btn-primary"><i class="icon-share-alt icon-white"></i> {% trans "Use in" %} #{{ request.session.current_order_code }}</a>
+ {% else %}
+ <a href="#" class="btn btn-primary disabled"><i class="icon-share-alt icon-white"></i> {% trans "Use in current order" %}</a>
+ {% endif %}
+ {% if device.pk %}
+ <a href="{% url 'devices-create_order' device.pk %}" class="btn"><i class="icon-plus"></i> {% trans "Create Service Order" %}</a>
+ {% endif %}
+ <div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if device.manual_url %}
+ <li><a href="{{ device.manual_url }}">{% trans "Service Manual" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Service Manual" %}</a></li>
+ {% endif %}
+ {% if device.exploded_view_url %}
+ <li><a href="{{ device.exploded_view_url }}">{% trans "Exploded View" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Exploded View" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ {% if device.is_apple_device %}
+ <li><a href="{% url 'devices-update_gsx_details' device.pk %}" class="spin">{% trans "Update Warranty Status" %}</a></li>
+ <li><a href="{% url 'devices-diagnostics' pk=device.id %}?a=get" data-modal="#modal">{% trans "Fetch Diagnostics" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#" class="spin">{% trans "Update Warranty Status" %}</a></li>
+ <li class="disabled"><a href="#" data-modal="#modal">{% trans "Fetch Diagnostics" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ {% if perms.servo.change_device %}
+ <li><a href="delete/" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ <div class="pull-right">
+ {% if perms.servo.change_device %}
+ <a href="edit/" class="btn">{% trans "Edit" %}</a>
+ {% endif %}
+ </div>
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "orders/list.html" with orders=device.order_set.all %}
+ </div>
+ {% if device.is_apple_device %}
+ <div class="tab-pane" id="tab3">
+ <div id="gsx-container" data-source="{% url 'devices-search_gsx' what='repairs' param='serialNumber' query=device.sn %}">
+ <div class="progress active">
+ <div class="bar" style="width:100%;" data-progress="0"></div>
+ </div>
+ <p class="text-center muted">{% trans "Fetching repairs..." %}</p>
+ </div>
+ </div>
+ {% endif %}
+</div>
+{% endblock fourth_column %}
+</div>
+
+{% block path %}
+ <li class="active"><span class="divider">/</span> {{ device.description }}</li>
+{% endblock path %}
diff --git a/servo/templates/dropdown_snippet.html b/servo/templates/dropdown_snippet.html
new file mode 100755
index 0000000..52d496f
--- /dev/null
+++ b/servo/templates/dropdown_snippet.html
@@ -0,0 +1,8 @@
+<div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">{{ title }} <span class="caret"></span></a>
+ <ul class="dropdown-menu">
+ {% for i in items %}
+ <li><a href="#"></a></li>
+ {% endfor %}
+ </ul>
+</div>
diff --git a/servo/templates/error.html b/servo/templates/error.html
new file mode 100644
index 0000000..db3ce21
--- /dev/null
+++ b/servo/templates/error.html
@@ -0,0 +1,49 @@
+{% load i18n %}
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="icon" type="image/png" href="{{ STATIC_URL }}images/favicon.png">
+ <link rel="apple-touch-icon" type="image/png" href="{{ STATIC_URL }}images/apple-touch-icon.png">
+ <link href="{{ STATIC_URL }}js/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}js/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}css/glyphicons.css" rel="stylesheet">
+ <title>{% trans "An Error Occurred" %}</title>
+ <style type="text/css">
+ label {
+ display: none;
+ }
+ </style>
+</head>
+<body>
+ <div class="main container">
+ {% block main %}
+ <div class="container-narrow">
+ <div class="row">
+ <div class="span12">
+ <div class="jumbotron">
+ {% if crashed %}
+ <h1>{% trans "Oops!" %}</h1>
+ <p class="lead">{% trans "It appears that an error has occurred." %} {% trans "I've notified the developers, but I'm sure they would really appreciate if you could briefly describe what you were doing before this happened." %}</p>
+ <form action="" method="post">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="pull-right">
+ <a class="btn btn-large" href="{% url 'home' %}">{% trans "Cancel" %}</a>
+ <button type="submit" class="btn btn-primary btn-large">{% trans "Submit" %}</button>
+ </div>
+ </form>
+ {% else %}
+ <h1>{% trans "Thanks!" %}</h1>
+ <p class="lead">{% trans "Your error report has been submitted. Thanks for helping make Servo better!" %}</p>
+ <a class="btn btn-large" href="{% url 'home' %}"><i class="icon-home"></i> {% trans "Return Home" %}</a>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ </div>
+ {% endblock main %}
+ </div>
+</body>
+</html>
diff --git a/servo/templates/form_buttons.html b/servo/templates/form_buttons.html
new file mode 100755
index 0000000..d67cd3f
--- /dev/null
+++ b/servo/templates/form_buttons.html
@@ -0,0 +1,5 @@
+{% load i18n %}
+<div class="form-actions">
+ <button type="button" class="btn" onclick="javascript:history.back();">{% trans "Back" %}</button>
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+</div>
diff --git a/servo/templates/form_field_snippet.html b/servo/templates/form_field_snippet.html
new file mode 100755
index 0000000..c4d1ab7
--- /dev/null
+++ b/servo/templates/form_field_snippet.html
@@ -0,0 +1,30 @@
+{% load i18n %}
+{% load servo_tags %}
+<div class="control-group{% for e in field.errors %} error{% endfor %} {{ classes }}">
+{% if field|widget_is:"CheckboxInput" %}
+ <div class="controls">
+ <label class="checkbox" for="{{ field.id }}">
+ {{ field }} {% trans field.label %}
+ {% if field.help_text %}
+ <span class="help-block">{% trans field.help_text %}</span>
+ {% endif %}
+ </label>
+ </div>
+{% elif field|widget_is:"CheckboxSelectMultiple" %}
+ <label class="control-label {{ field.css_classes }}">{% trans field.label %}</label>
+ <div class="controls">
+ {{ field }}
+ </div>
+{% else %}
+ <label class="control-label {{ field.css_classes }}" for="{{ field.id }}">{% trans field.label %}</label>
+ <div class="controls">
+ {{ field }}
+ {% if field.help_text %}
+ <span class="help-block">{% trans field.help_text %}</span>
+ {% endif %}
+ {% for e in field.errors %}
+ <span class="help-inline">{{ e }}</span>
+ {% endfor %}
+ </div>
+{% endif %}
+</div>
diff --git a/servo/templates/form_snippet.html b/servo/templates/form_snippet.html
new file mode 100755
index 0000000..c9264c8
--- /dev/null
+++ b/servo/templates/form_snippet.html
@@ -0,0 +1,7 @@
+{% for hidden in form.hidden_fields %}
+ {{ hidden }}
+{% endfor %}
+{% for field in form.visible_fields %}
+ {% include "form_field_snippet.html" with field=field %}
+{% endfor %}
+{{ form.media }}
diff --git a/servo/templates/generic/admin_list.html b/servo/templates/generic/admin_list.html
new file mode 100644
index 0000000..25a29c6
--- /dev/null
+++ b/servo/templates/generic/admin_list.html
@@ -0,0 +1,18 @@
+{% extends "admin/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block second_column %}
+<div class="span3">
+ <ul class="nav nav-list">
+ <li class="nav-header">{{ title }}</li>
+ {% for o in object_list %}
+ <li class="{% active_url request o.get_admin_url %}"><a href="{{ o.get_admin_url }}">{{ o.title|truncatechars:25 }}</a></li>
+ {% endfor %}
+ </ul>
+</div>
+<div class="span9">
+ {% block third_column %}
+ {% endblock third_column %}
+</div>
+{% endblock second_column %}
diff --git a/servo/templates/generic/button_dropdown.html b/servo/templates/generic/button_dropdown.html
new file mode 100755
index 0000000..39aba25
--- /dev/null
+++ b/servo/templates/generic/button_dropdown.html
@@ -0,0 +1,12 @@
+{% if items.count %}
+<div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-file"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% for i in items %}
+ <li><a href="{{ i.get_absolute_url }}">{{ i }}</a></li>
+ {% endfor %}
+ </ul>
+</div>
+{% endif %}
diff --git a/servo/templates/generic/delete.html b/servo/templates/generic/delete.html
new file mode 100755
index 0000000..0503ce5
--- /dev/null
+++ b/servo/templates/generic/delete.html
@@ -0,0 +1,17 @@
+{% extends "snippets/modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block body %}
+ {{ explanation }}
+{% endblock body %}
+
+{% block footer %}
+ <form method="post" action="{{ action }}">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/generic/index.html b/servo/templates/generic/index.html
new file mode 100755
index 0000000..379db1c
--- /dev/null
+++ b/servo/templates/generic/index.html
@@ -0,0 +1,23 @@
+{% extends "admin/index.html" %}
+{% load i18n %}
+
+{% block second_column %}
+<ul class="nav nav-tabs">
+ <li class="active"><a href="#">{% trans "Browse" %}</a></li>
+</ul>
+
+<table class="table">
+{% for item in object_list %}
+ <tr>
+ <td><a href="{{ item.id }}/edit/">{{ item.title }}</a></td>
+ <td style="text-align:right">
+ <div class="btn-group">
+ <a href="{{ item.pk }}/edit/" class="btn btn-warning" title="{% trans "Edit" %}"><i class="icon-pencil icon-white"></i></a>
+ <a href="{{ item.pk }}/delete/" class="btn btn-danger" data-modal="#modal" title="{% trans "Delete" %}"><i class="icon-trash icon-white"></i></a>
+ </div>
+ </td>
+ </tr>
+{% endfor %}
+</table>
+<a class="btn" href="new/"><i class="icon-plus"></i> {% trans "Create New" %}</a>
+{% endblock second_column %}
diff --git a/servo/templates/invoices/index.html b/servo/templates/invoices/index.html
new file mode 100755
index 0000000..94bf6ae
--- /dev/null
+++ b/servo/templates/invoices/index.html
@@ -0,0 +1,106 @@
+{% extends "products/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block first_column %}
+<ul class="nav nav-list">
+ <li class="nav-header">{% trans "Invoices" %}</li>
+ <li class="active"><a href="{% url 'invoices-index' %}">{% trans "Sales Invoices" %}</a></li>
+ <li><a href="{% url 'invoices-gsx_invoices' %}">{% trans "Apple Invoices" %}</a></li>
+</ul>
+{% endblock first_column %}
+
+{% block toolbar %}
+{% endblock toolbar %}
+
+{% block second_column %}
+{% block filter_bar %}
+<div class="row-fluid">
+ <div class="span12 well">
+ <form action="" method="post" class="form-inline">
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <div class="control-group">
+ <label class="control-label">&nbsp;</label>
+ <div class="controls">
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+</div>
+{% endblock filter_bar %}
+<div class="row-fluid">
+ <div class="span12">
+ <table class="table table-hover">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>{% trans "Order" %}</th>
+ <th>{% trans "Customer" %}</th>
+ <th>{% trans "Date Created" %}</th>
+ <th>{% trans "Status" %}</th>
+ <th>{% trans "Total" %}</th>
+ <th>{% trans "Date Paid" %}</th>
+ <th>{% trans "Payment Method" %}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for i in invoices %}
+ <tr>
+ <td>{{ i.pk|safe }}</td>
+ <td><a href="{{ i.order.get_absolute_url }}">{{ i.order.code }}</a></td>
+ <td>{{ i.customer_name }}</td>
+ <td>{{ i.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ i.order.status_name }}</td>
+ <td>{{ i.total_gross|currency }}</td>
+ <td>{{ i.paid_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ i.get_payment_methods|join:", " }}</td>
+ <td>
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'invoices-print_invoice' i.pk %}" class="window">{% trans "Print receipt" %}</a></li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="9" class="empty muted">{% trans "No invoices found" %}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td><strong>{{ total.total_net__sum|currency|default:"" }}<br/>{{ total_purchases.total__sum|currency|default:"" }}</strong></td>
+ <td><strong>{{ total_paid.total_net__sum|currency|default:"" }}</strong></td>
+ <td></td>
+ <td></td>
+ </tr>
+ </tfoot>
+ </table>
+ {% include "pagination.html" with items=invoices %}
+ </div>
+</div>
+{% endblock second_column %}
+
+{% block third_column %}
+{% endblock third_column %}
+
+{% block crumbs %}
+<li class="active"><span class="divider"></span> <a href="{% url 'invoices-index' %}">{% trans "Invoices" %}</a></li>
+{% endblock crumbs %}
diff --git a/servo/templates/invoices/view_invoice.html b/servo/templates/invoices/view_invoice.html
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/servo/templates/invoices/view_invoice.html
diff --git a/servo/templates/login.html b/servo/templates/login.html
new file mode 100755
index 0000000..d7e6fac
--- /dev/null
+++ b/servo/templates/login.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <link rel="icon" type="image/png" href="{{ STATIC_URL }}images/favicon.png">
+ <link rel="apple-touch-icon" type="image/png" href="{{ STATIC_URL }}images/apple-touch-icon.png">
+ <script src="{{ STATIC_URL }}js/jquery.min.js"></script>
+ <script src="{{ STATIC_URL }}js/bootstrap/js/bootstrap.min.js"></script>
+ <link href="{{ STATIC_URL }}js/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+ <link href="{{ STATIC_URL }}css/login.css" rel="stylesheet">
+ <title>{{ title }} | Servo</title>
+</head>
+<body>
+{% if messages %}
+ {% for message in messages %}
+ <div class="alert alert-block alert-{{ message.tags }}">
+ <button type="button" class="close" data-dismiss="alert">&times;</button>
+ <i class="icon-warning-sign"></i> <strong>{{ message }}</strong>
+ </div>
+ {% endfor %}
+ {% endif %}
+ <div class="login-container">
+ {% block content %}{% endblock content %}
+ </div>
+</body>
+</html>
diff --git a/servo/templates/modal.html b/servo/templates/modal.html
new file mode 100755
index 0000000..d6f75db
--- /dev/null
+++ b/servo/templates/modal.html
@@ -0,0 +1,19 @@
+{% load i18n %}
+<div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ <h3>{% block header %} {{ title }} {% endblock header %}</h3>
+ </div>
+ <div class="modal-body">
+ {% block body %}
+ {% trans "This action cannot be undone." %}
+ {% endblock body %}
+ </div>
+ <div class="modal-footer">
+ {% block footer %}
+ <button type="submit" class="btn btn-primary" data-dismiss="modal">{% trans "Done" %}</button>
+ {% endblock footer %}
+ </div>
+ </div>
+</div>
diff --git a/servo/templates/notes/edit_escalation.html b/servo/templates/notes/edit_escalation.html
new file mode 100755
index 0000000..c4e34c1
--- /dev/null
+++ b/servo/templates/notes/edit_escalation.html
@@ -0,0 +1,17 @@
+{% extends "notes/view_note.html" %}
+{% load i18n %}
+
+{% block note_buttons %}
+{% endblock note_buttons %}
+
+{% block view_message %}
+ <form action="{{ request.path }}" method="post" class="form-horizontal" enctype="multipart/form-data">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ <div class="form-actions">
+ <div class="pull-right">
+ <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
+ </div>
+ </div>
+ </form>
+{% endblock view_message %}
diff --git a/servo/templates/notes/find.html b/servo/templates/notes/find.html
new file mode 100755
index 0000000..1a55793
--- /dev/null
+++ b/servo/templates/notes/find.html
@@ -0,0 +1,39 @@
+{% extends "notes/list_notes.html" %}
+{% load i18n %}
+
+{% block second_row %}
+<div class="row-fluid" style="margin-top:30px">
+ <div class="span12">
+ <ul class="nav nav-tabs">
+ <li><a href="{% url 'notes-list_notes' 'inbox' %}">{% trans "Browse" %}</a></li>
+ <li class="active"><a href="{% url 'notes-find' %}">{% trans "Search" %}</a></li>
+ </ul>
+ </div>
+</div>
+<div class="row-fluid">
+ <div class="span12 well">
+ <form action="" method="get">
+ {% for field in form %}
+ <div class="control-group pull-left" style="margin-right:10px">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <div class="control-group">
+ <label class="control-label">&nbsp;</label>
+ <div class="controls">
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+</div>
+<div class="container-fluid">
+ <div class="row-fluid">
+ <div class="span12">
+ {% include "notes/search-results.html" %}
+ </div>
+ </div>
+</div>
+{% endblock second_row %}
diff --git a/servo/templates/notes/form.html b/servo/templates/notes/form.html
new file mode 100755
index 0000000..372da30
--- /dev/null
+++ b/servo/templates/notes/form.html
@@ -0,0 +1,112 @@
+{% extends "two_column_layout.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block first_column %}
+{% with note.order as order %}
+{% if order %}
+{% if order.customer %}
+{% include "orders/customer.html" with nodes=order.customer_list %}
+<hr/>
+{% endif %}
+{% for device in order.devices.all %}
+<p>
+ <strong>{{ device.description }}</strong><br/>
+ <small class="muted">{{ device.configuration }}</small><br/>
+ <small class="muted"><i class="icon-barcode"></i> {{ device.sn }}</small>
+</p>
+{% endfor %}
+<hr/>
+{% for p in order.products %}
+<small><strong>{{ p.code }}</strong></small><small class="pull-right">{{ p.price|currency }}</small><br/>
+<small class="muted">{{ p.title }}</small><br/>
+{% endfor %}
+<hr/>
+<strong>{% trans "Total" %}:</strong>
+<span class="pull-right"><strong>{{ order.gross_total|currency }}</strong></span>
+{% endif %}
+{% endwith %}
+{% endblock first_column %}
+
+{% block second_column %}
+
+<form action="" method="post" accept-charset="utf-8" enctype="multipart/form-data" class="form-horizontal">
+ {% csrf_token %}
+ {{ form.customer }}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Note" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Attachments" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "Labels" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "Escalation" %}</a></li>
+ </ul>
+ {{ formset.management_form }}
+ {{ form.order }}
+ {% if note.parent %}
+ {{ form.parent }}
+ {% endif %}
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.recipient %}
+ {% include "form_field_snippet.html" with field=form.subject %}
+ {% include "form_field_snippet.html" with field=form.sender %}
+ {% include "form_field_snippet.html" with field=form.body %}
+ {% include "form_field_snippet.html" with field=form.is_reported %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% for f in formset %}
+ {% include "form_snippet.html" with form=f %}
+ {% endfor %}
+ </div>
+ <div class="tab-pane" id="tab3">
+ {% include "form_field_snippet.html" with field=form.labels %}
+ </div>
+ <div class="tab-pane" id="tab4">
+ {% include "form_field_snippet.html" with field=escalation_form.issue_type %}
+ {% include "form_field_snippet.html" with field=escalation_form.status %}
+ {% include "form_field_snippet.html" with field=escalation_form.gsx_account %}
+ {% for k, v in contexts.items %}
+ <div class="control-group">
+ <select name="keys" class="control-label">
+ {% for i, f in fields %}
+ <option{% if i == k %} selected="selected"{% endif %} value="{{ i }}">{{ f }}</option>
+ {% endfor %}
+ </select>
+ <div class="controls">
+ <div class="input-append">
+ <input type="text" name="values" value="{{ v }}"/>
+ <a class="btn remove_field"><i class="icon-remove"></i></a>
+ </div>
+ </div>
+ </div>
+ {% endfor %}
+ <div class="control-group property">
+ <select name="keys" class="control-label">
+ {% for i, f in fields %}
+ <option value="{{ i }}">{{ f }}</option>
+ {% endfor %}
+ </select>
+ <div class="controls">
+ <div class="input-append">
+ <input type="text" name="values"/>
+ <a class="btn remove_field disabled"><i class="icon-remove"></i></a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="form-actions">
+ {% if note.pk %}
+ <a class="btn btn-danger" href="{% url 'notes-delete_note' note.pk %}" data-modal="#modal">{% trans "Delete" %}</a>
+ {% endif %}
+ <input type="submit" class="btn btn-primary" value="{% trans "Save" %}"/>
+ </div>
+</form>
+{% endblock second_column %}
+
+{% block crumbs %}
+{% if note.order %}
+ <li><a href="{{ order.get_queue_url }}">{{ order.get_queue_title }}</a> <span class="divider">/</span></li>
+ <li><a href="{{ order.get_absolute_url }}">{% trans "Order" %} {{ order.code }}</a> <span class="divider">/</span></li>
+ <li class="active">{% trans "Edit Note" %}</li>
+{% endif %}
+{% endblock crumbs %}
diff --git a/servo/templates/notes/list_notes.html b/servo/templates/notes/list_notes.html
new file mode 100755
index 0000000..fdbc853
--- /dev/null
+++ b/servo/templates/notes/list_notes.html
@@ -0,0 +1,75 @@
+{% extends "default.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+<div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-file"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'notes-create' %}">{% trans "New Message" %}</a></li>
+ <!--<li><a href="{% url 'notes-create_escalation' %}">{% trans "New Escalation" %}</a></li>//-->
+ </ul>
+</div>
+{% block note_buttons %}
+<div class="btn-group">
+ <a class="btn dropdown-toggle disabled" data-toggle="dropdown" href="#">
+ <i class="icon-flag"></i> <span class="caret"></span>
+ </a>
+</div>
+{% endblock note_buttons %}
+{% endblock toolbar %}
+
+{% block content %}
+
+{% block second_row %}
+<ul class="nav nav-tabs" style="margin-top:30px">
+ <li class="active"><a href="{% url 'notes-list_notes' 'inbox' %}">{% trans "Browse" %}</a></li>
+ <li><a href="{% url 'notes-find' %}">{% trans "Search" %}</a></li>
+</ul>
+
+<div class="row-fluid">
+ <div class="span2">
+ {% block first_column %}
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Messages" %}</li>
+ <li class="{% active request "inbox" %}"><a href="{% url 'notes-list_notes' kind="inbox" %}">{% trans "Inbox" %} <span class="badge pull-right">{{ inbox_count|safe }}</span></a></li>
+ <li class="{% active request "flagged" %}"><a href="{% url 'notes-list_notes' kind="flagged" %}">{% trans "Flagged" %}</a></li>
+ <li class="{% active request "sent" %}"><a href="{% url 'notes-list_notes' kind="sent" %}">{% trans "Sent" %}</a></li>
+ <!--<li class="{% active request "escalations" %}"><a href="{% url 'notes-list_notes' kind="escalations" %}">{% trans "Escalations" %}</a></li>//-->
+ </ul>
+ {% endblock first_column %}
+ </div>
+ <div class="span3">
+ {% block second_column %}
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans kind %}</li>
+ {% for note in notes %}
+ <li class="{% active request 'notes' kind note.pk 'view' %}">
+ <a href="{% url 'notes-view_note' kind note.pk %}">
+ <strong>{{ note.sender }}</strong><span class="pull-right">{{ note.created_at|date:"SHORT_DATE_FORMAT" }}</span>
+ <br/>
+ <small>{{ note.body|truncatechars:30 }}</small>
+ </a>
+ </li>
+ {% empty %}
+ <li class="disabled text-center"><a href="#">{% trans "No messages found" %}</a></li>
+ {% endfor %}
+ </ul>
+ {% include "pagination.html" with items=notes %}
+ {% endblock second_column %}
+ </div>
+ <div class="span7">
+ {% block view_message %}
+ <h2 class="muted text-center">{% trans "No message selected" %}</h2>
+ {% endblock view_message %}
+ </div>
+</div>
+{% endblock second_row %}
+
+{% endblock content %}
+
+{% block crumbs %}
+<li class="active">{% trans "Notes" %}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/notes/messages.html b/servo/templates/notes/messages.html
new file mode 100644
index 0000000..84bbd42
--- /dev/null
+++ b/servo/templates/notes/messages.html
@@ -0,0 +1,22 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Message Log" %}
+{% endblock header %}
+
+{% block body %}
+<table class="table">
+ <tbody>
+ {% for m in messages %}
+ <tr>
+ <td>{{ m.sent_at|date:"SHORT_DATETIME_FORMAT" }}</td>
+ <td>{{ m.recipient }}</td>
+ <td>{{ m.status }}</td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="3" class="muted empty">{% trans "No messages to display" %}</td></tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock body %}
diff --git a/servo/templates/notes/remove.html b/servo/templates/notes/remove.html
new file mode 100755
index 0000000..c79f4b1
--- /dev/null
+++ b/servo/templates/notes/remove.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Delete this note?" %}
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans "This will also delete any replies to this note." %}</p>
+{% endblock body %}
+
+{% block footer %}
+ <form action="{% url 'notes-delete_note' note.pk %}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/notes/search-results.html b/servo/templates/notes/search-results.html
new file mode 100755
index 0000000..a1ed78d
--- /dev/null
+++ b/servo/templates/notes/search-results.html
@@ -0,0 +1,25 @@
+{% load i18n %}
+{% load humanize %}
+{% load servo_tags %}
+
+<ul class="media-list">
+ {% for note in notes %}
+ <li class="media">
+ <a class="pull-left" href="#">
+ <img src="{{ note.created_by.get_avatar }}" alt="{{ note.created_by }}" title="{{ note.created_by }}" class="img-rounded avatar"/>
+ </a>
+ <div class="media-body">
+ <h5 class="media-heading">{{ note.get_sender_name }} {{ note.created_at|naturaltime }}{% if note.order %} <a href="{% url 'orders-edit' note.order.pk %}#note-{{ note.pk }}"><i class="icon-share-alt"></i></a>{% endif %}</h5>
+ {{ note.body|markdown }}
+ {% for a in note.attachments.all %}
+ <a class="label label-info window" href="{{ a.content.url }}"><i class="icon-download icon-white"></i> {{ a }}</a>
+ {% endfor %}
+ </div>
+ <hr/>
+ </li>
+ {% empty %}
+ <li class="muted empty">{% trans "No notes found" %}</li>
+ {% endfor %}
+</ul>
+
+{% include "pagination.html" with items=notes %}
diff --git a/servo/templates/notes/search.html b/servo/templates/notes/search.html
new file mode 100755
index 0000000..023bb07
--- /dev/null
+++ b/servo/templates/notes/search.html
@@ -0,0 +1,18 @@
+{% extends "notes/list_notes.html" %}
+{% load servo_tags %}
+{% load humanize %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'notes-create' %}" class="btn"><i class="icon-plus"></i> {% trans "Create Note" %}</a>
+{% endblock toolbar %}
+
+{% block content %}
+
+<div class="page-header">
+ <h1>{{ title }} <small>{{ subtitle }}</small></h1>
+</div>
+
+{% include "notes/search-results.html" %}
+
+{% endblock content %}
diff --git a/servo/templates/notes/templates.html b/servo/templates/notes/templates.html
new file mode 100755
index 0000000..e552791
--- /dev/null
+++ b/servo/templates/notes/templates.html
@@ -0,0 +1,10 @@
+<div class="btn-group pull-left">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-comment"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu template-select">
+ {% for t in templates %}
+ <li><a href="{{ t.get_absolute_url }}">{{ t.title }}</a></li>
+ {% endfor %}
+ </ul>
+</div>
diff --git a/servo/templates/notes/view_escalation.html b/servo/templates/notes/view_escalation.html
new file mode 100755
index 0000000..5609587
--- /dev/null
+++ b/servo/templates/notes/view_escalation.html
@@ -0,0 +1 @@
+{% extends "notes/view_note.html" %}
diff --git a/servo/templates/notes/view_note.html b/servo/templates/notes/view_note.html
new file mode 100755
index 0000000..05000f6
--- /dev/null
+++ b/servo/templates/notes/view_note.html
@@ -0,0 +1,38 @@
+{% extends "notes/list_notes.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block note_buttons %}
+ <div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-flag"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'notes-toggle_flag' note.pk 'read' %}" class="nofollow">{{ note.get_read_title }}</a></li>
+ <li><a href="{% url 'notes-toggle_flag' note.pk 'flagged' %}" class="nofollow">{{ note.get_flagged_title }}</a></li>
+ </ul>
+ </div>
+{% endblock note_buttons %}
+
+{% block view_message %}
+ <div class="message-header">
+ <p>{% trans "From" %}: {{ note.sender }}</p>
+ {% if note.order.id %}
+ <p>{% trans "Service Order" %}: <a href="{% url 'orders-edit' note.order.id %}">{{ note.order.code }}</a></p>
+ {% endif %}
+ <p>{% trans "Created" %}: {{ note.created_at|relative_date }}</p>
+ <p>{% trans "Subject" %}: {{ note.subject }}</p>
+ </div>
+ <hr/>
+ {{ note.body|markdown }}
+ {% for a in note.attachments.all %}
+ <a class="label label-info window" href="{{ a.content.url }}"><i class="icon-download icon-white"></i> {{ a }}</a>
+ {% endfor %}
+ <div class="form-actions">
+ <div class="pull-right">
+ <a href="{% url 'servo.views.order.create' note_id=note.id %}" class="btn {% if note.order.id %} disabled {% endif %}">{% trans "Create Order" %}</a>
+ <a href="{% url 'notes-delete_note' note.id %}" class="btn btn-danger" data-modal="#modal">{% trans "Delete" %}</a>
+ <a href="{% url 'servo.views.note.edit' parent=note.id %}" class="btn btn-primary">{% trans "Reply" %}</a>
+ </div>
+ </div>
+{% endblock view_message %}
diff --git a/servo/templates/oauth/challenge.html b/servo/templates/oauth/challenge.html
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/servo/templates/oauth/challenge.html
diff --git a/servo/templates/orders/batch_process.html b/servo/templates/orders/batch_process.html
new file mode 100644
index 0000000..996eceb
--- /dev/null
+++ b/servo/templates/orders/batch_process.html
@@ -0,0 +1,14 @@
+{% extends "orders/index.html" %}
+{% load bootstrap3 %}
+{% load i18n %}
+
+{% block content %}
+ <h2>{% trans "Batch Processing" %}</h2>
+ <form action="" method="post">
+ {% csrf_token %}
+ {% bootstrap_form form %}
+ {% buttons %}
+ <button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
+ {% endbuttons %}
+ </form>
+{% endblock content %}
diff --git a/servo/templates/orders/checklists.html b/servo/templates/orders/checklists.html
new file mode 100755
index 0000000..b2f9e47
--- /dev/null
+++ b/servo/templates/orders/checklists.html
@@ -0,0 +1,10 @@
+{% load servo_tags %}
+{% for c in checklists %}
+ <h4>{{ c.title }} ({{ order.checklistitemvalue_set.all.count }}/{{ c.checklistitem_set.all.count }})</h4>
+ {% for i in c.checklistitem_set.all %}
+ <label class="checkbox" style="margin-left:8x">
+ <input type="checkbox" data-url="{% url 'orders-toggle_task' order.id i.id %}" class="toggle" {% if order|is_item_complete:i %}checked="checked"{% endif %}/> {{ i.title }} <i class="muted">{{ order|item_completed_by:i }}</i>
+ </label>
+ {% if forloop.last %}<hr/>{% endif %}
+ {% endfor %}
+{% endfor %}
diff --git a/servo/templates/orders/close.html b/servo/templates/orders/close.html
new file mode 100755
index 0000000..b80f862
--- /dev/null
+++ b/servo/templates/orders/close.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans 'Close order' %} {{ order.code }}?
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans "This order will no longer be editable." %}</p>
+{% endblock body %}
+
+{% block footer %}
+<form action="{{ action }}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button class="btn btn-primary" type="submit">{% trans "Close" %}</button>
+</form>
+{% endblock footer %}
diff --git a/servo/templates/orders/close_repair.html b/servo/templates/orders/close_repair.html
new file mode 100755
index 0000000..db50087
--- /dev/null
+++ b/servo/templates/orders/close_repair.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Complete the GSX repair?" %}
+{% endblock header %}
+
+{% block body %}
+ {% trans "The GSX repair will no longer be editable." %}
+{% endblock body %}
+
+{% block footer %}
+ <form method="post" action="close/">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger spin">{% trans "Complete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/orders/customer.html b/servo/templates/orders/customer.html
new file mode 100755
index 0000000..ccd9184
--- /dev/null
+++ b/servo/templates/orders/customer.html
@@ -0,0 +1,52 @@
+{% load i18n %}
+{% load mptt_tags %}
+{% load servo_tags %}
+{% recursetree nodes %}
+<address>
+ <strong><a href="{{ node.get_absolute_url }}"><i class="{{ node.get_icon }}"></i> {{ node.name }}</a></strong>
+ <div class="btn-group pull-right">
+ <a class="btn btn-small dropdown-toggle{% if order.is_closed %} disabled{% endif %}" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'orders-choose_customer' order.pk %}" data-modal="#modal">{% trans "Choose" %}</a></li>
+ {% if node.is_company %}
+ <li><a href="{% url 'customers-create_contact' parent_id=node.id %}">{% trans "Add contact" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Add contact" %}</a></li>
+ {% endif %}
+ <li><a href="{% url 'orders-create_with_customer' customer_id=node.id %}">{% trans "Create Order" %}</a></li>
+ <li class="divider"></li>
+ <li><a href="{% url 'customers-edit_customer' group=node.get_group pk=node.pk %}">{% trans "Edit" %}</a></li>
+ {% if node.is_leaf_node %}
+ <li><a href="{% url 'orders-remove_customer' pk=order.pk customer_id=node.pk %}" data-modal="#modal">{% trans "Remove" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#" data-modal="#modal">{% trans "Remove" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ <br/>
+ {% if node.email %}
+ <a href="{% url 'notes-create_with_to_and_order' recipient=node.email order_id=order.id %}">{{ node.email }}</a><br/>
+ {% endif %}
+ {% if node.phone %}
+ <a href="{% url 'notes-create_with_to_and_order' recipient=node.get_standard_phone order_id=order.id %}">{{ node.get_national_phone }}</a><br/>
+ {% endif %}
+ {% if node.street_address %}
+ {{ node.street_address }}<br/>
+ {{ node.zip_code }} {{ node.city }}
+ {% endif %}
+ {% with node.fullprops.items as props %}
+ {% if props %}
+ <br/>
+ {% for k, v in props %}
+ {{ k }}: {{ v }}<br/>
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+ {% if node.notes %}
+ <p><small class="muted">{{ node.notes|default:"" }}</small></p>
+ {% endif %}
+</address>
+{{ children }}
+{% endrecursetree %}
diff --git a/servo/templates/orders/delete_order.html b/servo/templates/orders/delete_order.html
new file mode 100755
index 0000000..ee44fc6
--- /dev/null
+++ b/servo/templates/orders/delete_order.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with id=order.code%}Really delete order {{ id }}? {% endblocktrans %}
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans "This will also delete all the order's notes. This action cannot be undone." %}</p>
+{% endblock body %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger pull-right">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/orders/devices.html b/servo/templates/orders/devices.html
new file mode 100755
index 0000000..271a4ec
--- /dev/null
+++ b/servo/templates/orders/devices.html
@@ -0,0 +1,139 @@
+{% load i18n %}
+{% load servo_tags %}
+
+{% for d in order.orderdevice_set.all %}
+{% with d.device as device %}
+<div class="row-fluid">
+ <div class="span3">
+ <img src="{{ device.get_photo }}" alt="{{ device.description }}" title="{{ device.description }}"/>
+ </div>
+ <div class="span9">
+ <h3>{{ device.description }}</h3>
+ {% if d.is_repeat_service %}
+ <span class="label label-important pull-right">{% trans "Repeat Service" %}</span>
+ {% endif %}
+ <dl class="dl-horizontal">
+ <dt>{% trans "Warranty Status" %}</dt>
+ <dd>{{ device.get_warranty_status_display }}</dd>
+ <dt>{% trans "Purchase Date" %}</dt>
+ <dd>{{ device.purchased_on|date:"SHORT_DATE_FORMAT"|default:"-" }}, {{ device.get_purchase_country|default:"-" }}</dd>
+ <dt>{% trans "Serial Number" %}</dt>
+ <dd>{{ device.sn|default:"-" }}</dd>
+ {% if device.configuration %}
+ <dt>{% trans "Configration" %}</dt>
+ <dd>{{ device.configuration }}</dd>
+ {% endif %}
+ {% if device.imei %}
+ <dt>{% trans "IMEI" %}</dt>
+ <dd>{{ device.imei }}</dd>
+ <dt>{% trans "Applied Activation Policy" %}</dt>
+ <dd>{{ device.applied_activation_policy }}</dd>
+ <dt>{% trans "Initial Activation Policy" %}</dt>
+ <dd>{{ device.initial_activation_policy }}</dd>
+ <dt>{% trans "Find My iPhone" %}</dt>
+ <dd>{{ device.get_fmip_status }}</dd>
+ <dt>{% trans "Unlocked" %}</dt>
+ <dd>{{ device.unlocked|yesno }}</dd>
+ {% endif %}
+ {% if device.username %}
+ <dt>{% trans "Username" %}</dt>
+ <dd>{{ device.username }}</dd>
+ {% endif %}
+ {% if device.password %}
+ <dt>{% trans "Password" %}</dt>
+ <dd>{{ device.password }}</dd>
+ {% endif %}
+ {% if device.get_coverage_details %}
+ <dt>{% trans "Details" %}</dt>
+ <dd>{{ device.get_coverage_details|join:"<br/>" }}</dd>
+ {% endif %}
+ {% if device.notes %}
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ device.notes }}</dd>
+ {% endif %}
+ {% with device|device_accessories:order as accessories %}
+ {% if accessories.count %}
+ <dt>{% trans "Accessories" %}</dt>
+ <dd>{{ accessories|join:", " }}</dd>
+ {% endif %}
+ {% endwith %}
+ </dl>
+ {% if order.queue %}
+ <a href="{% url 'orders-list_parts' order.id device.id order.queue_id %}" class="btn btn-small"><i class="icon-wrench"></i> {% trans "Parts" %}</a>
+ {% else %}
+ <a class="btn btn-small disabled" title="{% trans "Assign order to a queue first" %}"><i class="icon-wrench"></i> {% trans "Parts" %}</a>
+ {% endif %}
+ {% if order.is_editable %}
+ <a href="{% url 'orders-accessories' order.id device.id %}" class="btn btn-small" data-modal="#modal"><i class="icon-hdd"></i> {% trans "Accessories" %}</a>
+ {% else %}
+ <a class="btn btn-small disabled" href="#"><i class="icon-hdd"></i> {% trans "Accessories" %}</a>
+ {% endif %}
+ <a href="{% url 'orders-history' order.pk device.pk %}" class="btn btn-small" data-modal="#modal"><i class="icon-time"></i> {% trans "History" %}</a>
+ <div class="btn-group">
+ <a class="btn btn-small dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if order.can_create_carryin and device.can_create_carryin %}
+ <li><a href="{% url 'repairs-create_repair' order.id device.id 'CA' %}">{% trans "Create Carry-In Repair" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#" title="{% trans "Device has open GSX repairs" %}">{% trans "Create Carry-In Repair" %}</a></li>
+ {% endif %}
+ {% if order.can_create_carryin and device.can_create_carryin and device.has_onsite %}
+ <li><a href="{% url 'repairs-create_repair' order.id device.id 'ON' %}">{% trans "Create Onsite Repair" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Create Onsite Repair" %}</a></li>
+ {% endif %}
+ {% if order.can_create_carryin and device.can_create_carryin and 0 %}
+ <li><a href="{% url 'repairs-create_repair' order.id device.id 'RR' %}">{% trans "Create Whole-Unit Exchange" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Create Whole-Unit Exchange" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a href="{% url 'devices-update_gsx_details' device.pk %}" class="spin">{% trans "Update Warranty Status" %}</a></li>
+ <li class="divider"></li>
+ {% if device.is_ios %}
+ <li><a href="{% url 'devices-diagnostics' device.pk %}?a=init&amp;order={{ order.pk|safe }}" data-modal="#modal">{% trans "Initiate Diagnostics" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#" data-modal="#modal">{% trans "Fetch Diagnostics" %}</a></li>
+ {% endif %}
+ <li><a href="{% url 'devices-diagnostics' device.pk %}" data-modal="#modal">{% trans "Fetch Diagnostics" %}</a></li>
+ <li class="divider"></li>
+ <li><a href="{{ device.get_absolute_url }}">{% trans "Show Device" %}</a></li>
+ {% if device.sn == '' %}
+ <li class="disabled"><a href="#" target="_blank">{% trans "Show Barcode" %}</a></li>
+ {% else %}
+ <li><a href="{% url 'barcodes-view' device.sn %}?f=svg" target="_blank">{% trans "Show Barcode" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+<div class="pull-right">
+ <div class="btn-group">
+ {% if d.should_report %}
+ <a href="{% url 'orders-report_device' order.pk d.pk %}" class="btn btn-success btn-small nofollow active" title="{% trans "Report" %}" data-toggle="button"><i class="icon-ok"></i></a>
+ {% else %}
+ <a href="{% url 'orders-report_device' order.pk d.pk %}" class="btn btn-success btn-small nofollow" title="{% trans "Report" %}" data-toggle="button"><i class="icon-ok icon-white"></i></a>
+ {% endif %}
+ </div>
+ <div class="btn-group">
+ {% if order.is_editable %}
+ <a href="{{ device.get_absolute_url }}edit/" class="btn btn-warning btn-small" title="{% trans "Edit" %}"><i class="icon-pencil icon-white"></i></a>
+ <a href="{% url 'orders-delete_device' order.id device.id %}" class="btn btn-small btn-danger" data-modal="#modal" title="{% trans "Remove" %}"><i class="icon-trash icon-white"></i></a>
+ {% endif %}
+ </div>
+</div>
+</div>
+</div>
+<hr/>
+{% endwith %}
+{% empty %}
+<div class="row-fluid">
+ <div class="span12">
+ <div class="empty well"><h3 class="muted">{% trans "Order doesn't contain any devices" %}</h3></div>
+ </div>
+</div>
+{% endfor %}
+<p class="clearfix">
+ <a href="{% url 'devices-choose' order.pk %}" class="btn pull-right {% if order.is_closed %}disabled{% endif %}" data-modal="#modal"><i class="icon-plus"></i> {% trans "Add Device" %}</a>
+</p>
+<hr/>
diff --git a/servo/templates/orders/dispatch.html b/servo/templates/orders/dispatch.html
new file mode 100755
index 0000000..f7ea0fb
--- /dev/null
+++ b/servo/templates/orders/dispatch.html
@@ -0,0 +1,115 @@
+{% extends "orders/edit.html" %}
+{% load staticfiles %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block content %}
+<div class="page-header">
+ {% with order.status_title as stat %}
+ <h3 style="margin-top:0px;line-height:30px" title="{{ title }}">{{ title|truncatechars:40 }}<br/>
+ <img src="{% static order.get_status_img %}" title="{{ stat }}" alt="{{ stat }}"/><small style="padding-left:5px">{{ order.get_title }} {% if order.user %}({{ order.user.get_full_name }}){% endif %}</small></h3>
+ {% endwith %}
+</div>
+<form method="post" action="" accept-charset="utf8">
+ {% csrf_token %}
+ {{ formset.management_form }}
+ <div class="row-fluid">
+ <div class="span3">
+ <div class="row-fluid">
+ <fieldset>
+ <legend>{% trans "Customer" %}</legend>
+ <div class="row-fluid">
+ {% include "form_field_snippet.html" with field=form.customer_name %}
+ </div>
+ <div class="row-fluid">
+ {% include "form_field_snippet.html" with field=form.customer_email %}
+ </div>
+ <div class="row-fluid">
+ {% include "form_field_snippet.html" with field=form.customer_phone %}
+ </div>
+ <div class="row-fluid">
+ {% include "form_field_snippet.html" with field=form.customer_address %}
+ </div>
+ <div class="row-fluid">
+ {% include "form_field_snippet.html" with field=form.reference %}
+ </div>
+ </fieldset>
+ </div>
+ <div class="row-fluid">
+ <fieldset>
+ <legend>{% trans "Payment" %}</legend>
+ {% for f in formset %}
+ <div class="row-fluid formrow">
+ <div class="span8">
+ {{ f.created_by }}
+ {{ f.method }}
+ </div>
+ <div class="span4">
+ {{ f.amount }}
+ </div>
+ </div>
+ {% endfor %}
+ <button class="btn clonerow" type="button" data-prefix="payment"><i class="icon-plus"></i></button>
+ </fieldset>
+ </div>
+ </div>
+ <div class="span9">
+ <fieldset>
+ <legend>{% trans "Products" %}</legend>
+ <table class="table">
+ <thead>
+ <tr>
+ <th style="width:20px"></th>
+ <th>{% trans "Product" %}</th>
+ <th style="width:50px">{% trans "Qty" %}</th>
+ <th style="width:100px">{% trans "Price" %}</th>
+ <th style="width:100px">{% trans "Price w/ tax" %}</th>
+ <th style="width:100px">{% trans "Total" %}</th>
+ </tr>
+ </thead>
+ <tbody>{% for p in products %}
+ <tr>
+ <td><input type="checkbox" name="items" value="{{ p.id|safe }}" checked="checked" class="toggle_row"/></td>
+ <td><strong>{{ p.product.code }}</strong><br/>{{ p.product.title }}</td>
+ <td class="amount">{{ p.amount }}</td>
+ <td class="net">{{ p.price_notax|currency }}</td>
+ <td class="tax">{{ p.price|currency }}</td>
+ <td class="gross">{{ p.total_gross|currency }}</td>
+ </tr>{% endfor %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td></td>
+ <td style="text-align:right">{% trans "Total" %}:</td>
+ <td>{{ totals.amount }}</td>
+ <td>{{ form.total_net }}</td>
+ <td>{{ form.total_tax }}</td>
+ <td>{{ form.total_gross }}</td>
+ </tr>
+ </tfoot>
+ </table>
+ </fieldset>
+ </div>
+ </div>
+ <div class="form-actions">
+ <div class="pull-right">
+ <a class="btn btn-default" href="{{ order.get_absolute_url }}">{% trans "Cancel" %}</a>
+ <button type="submit" class="btn btn-primary">{% trans "Dispatch" %}</button>
+ </div>
+ </div>
+</form>
+{% endblock content %}
+
+{% block crumbs %}
+ <li><a href="{{ order.get_queue_url }}">{{ order.get_queue_title }}</a> <span class="divider">/</span></li>
+ <li><a href="{% url 'orders-edit' order.id %}">{% trans "Order" %} {{ order.get_footer }}</a> <span class="divider">/</span></li>
+ <li class="active">{% trans "Dispatch" %}</li>
+{% endblock crumbs %}
+
+{% block media %}
+<script type="text/javascript">
+ $('.clonerow').click(function() {
+ cloneMore('.formrow:last', 'payment', true);
+ });
+</script>
+{% endblock media %}
diff --git a/servo/templates/orders/edit.html b/servo/templates/orders/edit.html
new file mode 100755
index 0000000..c340585
--- /dev/null
+++ b/servo/templates/orders/edit.html
@@ -0,0 +1,115 @@
+{% extends "default.html" %}
+{% load staticfiles %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+{% include "orders/toolbar.html" %}
+{% endblock toolbar %}
+
+{% block content %}
+ <div class="page-header">
+ <div class="row-fluid">
+ <div class="span6">
+ {% with order.status_title as stat %}
+ <h3 style="margin-top:0px;line-height:30px" title="{{ title }}">{{ title|truncatechars:40 }}<br/>
+ <img src="{% static order.get_status_img %}" title="{{ stat }}" alt="{{ stat }}"/><small style="padding-left:5px">{{ order.get_title }} {% if order.user %}({{ order.user.get_full_name }}){% endif %}</small></h3>
+ {% endwith %}
+ </div>
+ <div class="span6">
+ {% if locations.count > 1 %}
+ <div class="pull-right" style="padding:5px">
+ <div class="btn-group">
+ <a class="btn dropdown-toggle{% if order.is_closed %} disabled{% endif %}" data-toggle="dropdown" href="#" title="{% trans "Checkin Location" %}: {{ order.checkin_location.title }}">
+ <i class="halflings log_in"></i> {{ order.checkin_location.title|truncatechars:10 }} <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% for l in locations %}
+ <li{% ifequal l order.checkin_location %} class="active"{% endifequal %}><a href="{% url 'orders-update' order.pk 'checkin' l.pk %}">{{ l.title }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="btn-group">
+ <a class="btn dropdown-toggle{% if order.is_closed %} disabled{% endif %}" data-toggle="dropdown" href="#" title="{% trans "Current Location" %}: {{ order.location.title }}">
+ <i class="icon-map-marker"></i> {{ order.location.title|truncatechars:10 }} <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% for l in locations %}
+ <li{% ifequal l order.location %} class="active"{% endifequal %}><a href="{% url 'orders-update' order.pk 'location' l.pk %}">{{ l.title }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="btn-group">
+ <a class="btn dropdown-toggle{% if order.is_closed %} disabled{% endif %}" data-toggle="dropdown" href="#" title="{% trans "Checkout Location" %}: {{ order.checkout_location.title }}">
+ <i class="halflings log_out"></i> {{ order.checkout_location.title|truncatechars:10 }} <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu pull-right">
+ {% for l in locations %}
+ <li{% ifequal l order.checkout_location %} class="active"{% endifequal %}><a href="{% url 'orders-update' order.pk 'checkout' l.pk %}">{{ l.title }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ <ul class="nav nav-tabs">
+ {% block tabs %}
+ {% include "orders/tabs.html" with active='order' %}
+ {% endblock tabs %}
+ </ul>
+ <div class="row-fluid">
+ <div class="span3" id="order_meta">
+ {% block first_column %}
+ {% if order.customer %}
+ {% include "orders/customer.html" with nodes=customer %}
+ {% else %}
+ <div class="well text-center">
+ <a href="{% url 'orders-choose_customer' order.pk %}" class="btn" data-modal="#modal"><i class="icon-search"></i> {% trans "Select Customer" %}</a>
+ </div>
+ {% endif %}
+ <hr/>
+ {% include "orders/checklists.html" %}
+ {% include "orders/followers.html" %}
+ <hr/>
+ <form method="post" action="{% url 'orders-update' order.pk "place" 0 %}" style="margin-bottom:0px">
+ {% csrf_token %}
+ <label><i class="icon-map-marker"></i> {% trans "Place" %}</label>
+ <input type="text" name="place" class="input typeahead span12" placeholder="{% trans "Enter place" %}" data-provide="typeahead" data-source="{% url 'api-places' %}"/>
+ </form>
+ {% if order.place %}
+ <div class="clearfix">
+ <span class="label label-info">{{ order.place }}</span>
+ </div>
+ {% else %}
+ <p class="muted text-center">{% trans "No place" %}</p>
+ {% endif %}
+ <hr/>
+ <form method="post" action="{% url 'orders-update' order.pk "label" 0 %}" style="margin-bottom:0px">
+ {% csrf_token %}
+ <label><i class="icon-tag"></i> {% trans "Label" %}</label>
+ <input type="text" name="label" class="input typeahead span12" placeholder="{% trans "Enter label" %}" data-provide="typeahead" data-source="{% url 'api-tags' %}"/>
+ </form>
+ {% for t in order.tags.all %}
+ <span class="label label-{{ t.color }}"><div class="pull-left">{{ t.title }}</div><a class="close" href="{% url 'orders-toggle_tag' order.pk t.pk %}"><i class="icon-remove icon-white"></i></a></span>
+ {% empty %}
+ <p class="muted text-center">{% trans "No labels" %}</p>
+ {% endfor %}
+ {% endblock first_column %}
+ </div><!-- //# order sidebar -->
+
+ <div class="span9">
+ {% block second_column %}
+ {% include "orders/devices.html" %}
+ {% include "orders/notes.html" with notes=order.notes %}
+ {% include "orders/products.html" %}
+ {% endblock second_column %}
+ </div>
+ </div>
+ {% endblock content %}
+
+ {% block crumbs %}
+ <li><a href="{{ order.get_queue_url }}">{{ order.get_queue_title }}</a> <span class="divider">/</span></li>
+ {% block morecrumbs %}<li class="active">{% trans "Order" %} {{ order.get_footer }}</li>{% endblock morecrumbs %}
+ {% endblock crumbs %}
diff --git a/servo/templates/orders/edit_product.html b/servo/templates/orders/edit_product.html
new file mode 100755
index 0000000..0bcb710
--- /dev/null
+++ b/servo/templates/orders/edit_product.html
@@ -0,0 +1,33 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block body %}
+
+<form method="post" action="{{ request.path }}" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_field_snippet.html" with field=form.title %}
+ {% include "form_field_snippet.html" with field=form.amount %}
+ {% include "form_field_snippet.html" with field=form.price_category %}
+ {% include "form_field_snippet.html" with field=form.price %}
+ {% if product.is_serialized %}
+ {% include "form_field_snippet.html" with field=form.sn %}
+ {% include "form_field_snippet.html" with field=form.kbb_sn %}
+ {% include "form_field_snippet.html" with field=form.imei %}
+ {% endif %}
+ {% if product.component_code %}
+ {% include "form_field_snippet.html" with field=form.comptia_code %}
+ {% include "form_field_snippet.html" with field=form.comptia_modifier %}
+ {% endif %}
+ {% include "form_field_snippet.html" with field=form.should_report %}
+</form>
+
+<script type="text/javascript">
+ {% autoescape off %}
+ var prices = {{ prices }};
+ {% endautoescape %}
+ $('#id_price_category').change(function(){
+ $('#id_price').val(prices[$(this).val()]);
+ });
+</script>
+
+{% endblock body %}
diff --git a/servo/templates/orders/events.html b/servo/templates/orders/events.html
new file mode 100755
index 0000000..389e22a
--- /dev/null
+++ b/servo/templates/orders/events.html
@@ -0,0 +1,21 @@
+{% extends "orders/edit.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block tabs %}
+{% include "orders/tabs.html" with active='events' %}
+{% endblock tabs %}
+
+{% block second_column %}
+<table class="table">
+ <tbody>
+ {% for event in order.events.all %}
+ <tr>
+ <td><img src="{{ STATIC_URL }}images/{{ event.get_icon }}.png" alt="{{ event.description }}" class="icon"/></td>
+ <td style="width:55%">{{ event.description }}</td>
+ <td style="text-align:right">{{ event.triggered_by }}, {{ event.triggered_at|relative_date }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock second_column %}
diff --git a/servo/templates/orders/followers.html b/servo/templates/orders/followers.html
new file mode 100755
index 0000000..ce020bd
--- /dev/null
+++ b/servo/templates/orders/followers.html
@@ -0,0 +1,28 @@
+{% load i18n %}
+
+<form method="post" action="{% url 'orders-update' order.pk "user" 0 %}" style="margin-bottom:0px">
+ {% csrf_token %}
+ <label><i class="icon-user"></i> {% trans "Followers" %}</label>
+ <input type="text" name="user" class="input typeahead span12" placeholder="{% trans "Enter name" %}" data-provide="typeahead" data-source="{% url 'api-users' %}?is_active=1"/>
+</form>
+{% for i in followers %}
+<div class="clearfix">
+ {% with i.get_full_name|truncatechars:26 as fullname %}
+ {% if i == order.user %}
+ <span class="label label-info pull-left">{{ fullname }}</span>
+ {% else %}
+ <span class="label pull-left">{{ fullname }}</span>
+ {% endif %}
+ {% endwith %}
+ <div class="btn-group pull-right">
+ {% if i == order.user %}
+ <a href="#" class="btn btn-small btn-primary active disabled" title="{% trans "Make primary" %}"><i class="icon-user icon-white"></i></a>
+ {% else %}
+ <a href="{% url 'orders-update' order.pk 'user' i.pk %}" class="btn btn-small" title="{% trans "Make primary" %}"><i class="icon-user"></i></a>
+ {% endif %}
+ <a href="{% url 'orders-remove_user' order.pk i.pk %}" class="btn btn-small" title="{% trans "Remove" %}"><i class="icon-remove"></i></a>
+ </div>
+</div>
+{% empty %}
+<p class="muted text-center">{% trans "No followers" %}</p>
+{% endfor %}
diff --git a/servo/templates/orders/gsx_repair_form.html b/servo/templates/orders/gsx_repair_form.html
new file mode 100755
index 0000000..512ce5a
--- /dev/null
+++ b/servo/templates/orders/gsx_repair_form.html
@@ -0,0 +1,94 @@
+{% extends "default.html" %}
+{% load staticfiles %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block content %}
+<div class="page-header">
+{% with order.status_title as stat %}
+ <h3 style="margin-top:0px;line-height:30px" title="{{ title }}">{{ title|truncatechars:40 }}<br/>
+ <img src="{% static order.get_status_img %}" title="{{ stat }}" alt="{{ stat }}"/><small style="padding-left:5px">{{ order.get_title }} {% if order.user %}({{ order.user.get_full_name }}){% endif %}</small></h3>
+{% endwith %}
+</div>
+<ul class="nav nav-tabs">
+ {% block tabs %}
+ {% include "orders/tabs.html" %}
+ {% endblock tabs %}
+</ul>
+
+<div class="row-fluid">
+ <form method="post" accept-charset="utf-8" action="#" class="form-horizontal spin" enctype="multipart/form-data" id="repair_form">
+ <div class="span3">
+ {% csrf_token %}
+ {{ repair_form.device }}
+ <strong>{{ device.description }}</strong>
+ <p>
+ <small class="muted">{{ device.get_warranty_status_display }}</small><br/>
+ <small class="muted">{{ device.configuration }}</small><br/>
+ <small class="muted"><i class="icon-barcode"></i> {{ device.sn }}</small>
+ </p>
+ <hr/>
+ <strong>{% trans "Parts" %}</strong>
+ <div class="async" data-url="{% url 'repairs-check_parts' repair.pk %}"><p class="text-center muted">{% trans "Checking warranty" %}...</p></div>
+ <hr/>
+ <strong>{% trans "Total" %}:</strong>
+ <span class="pull-right"><strong>{{ order.gross_total|currency }}</strong></span>
+ </div>
+ <div class="span9">
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Repair" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Details" %}</a></li>
+ {% if component_form %}
+ <li><a href="#tab4" data-toggle="tab">{% trans "Components" %}</a></li>
+ {% endif %}
+ <li><a href="#tab3" data-toggle="tab">{% trans "Customer" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=repair_form.symptom %}
+ {% include "form_field_snippet.html" with field=repair_form.diagnosis %}
+ {% include "form_field_snippet.html" with field=repair_form.tech_id %}
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "form_field_snippet.html" with field=repair_form.notes %}
+ {% include "form_field_snippet.html" with field=repair_form.unit_received_at %}
+ {% include "form_field_snippet.html" with field=repair_form.attachment %}
+ {% include "form_field_snippet.html" with field=repair_form.reference %}
+ {% include "form_field_snippet.html" with field=repair_form.consumer_law %}
+ {% include "form_field_snippet.html" with field=repair_form.request_review %}
+ {% if repair.can_mark_complete %}
+ {% include "form_field_snippet.html" with field=repair_form.replacement_sn %}
+ {% include "form_field_snippet.html" with field=repair_form.mark_complete %}
+ {% endif %}
+ </div>
+ <div class="tab-pane" id="tab3">
+ {% include "form_snippet.html" with form=customer_form %}
+ </div>
+ <div class="tab-pane" id="tab4">
+ {% include "form_snippet.html" with form=component_form %}
+ </div>
+ </div>
+ <div class="form-actions">
+ <label class="checkbox pull-left">
+ <input type="checkbox" name="confirm" id="id_confirm"> {% trans "Confirm" %}
+ </label>
+ <div class="pull-right">
+ {% if repair.pk %}
+ <a class="btn btn-danger" href="{% url 'repairs-delete_repair' repair.pk %}" data-modal="#modal">{% trans "Delete" %}</a>
+ {% endif %}
+ <button type="submit" class="btn" id="save-bulk-return" data-placeholder="{% trans "Submit" %}">{% trans "Save" %}</button>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+{% endblock content %}
+
+{% block crumbs %}
+<li><a href="{{ order.get_queue_url }}">{{ order.get_queue_title }}</a> <span class="divider">/</span></li>
+<li class="active">{% trans "Order" %} {{ order.code }}</li>
+{% endblock crumbs %}
+
+{% block media %}
+{{ repair_form.media }}
+{% endblock media %}
diff --git a/servo/templates/orders/history.html b/servo/templates/orders/history.html
new file mode 100644
index 0000000..5a908b9
--- /dev/null
+++ b/servo/templates/orders/history.html
@@ -0,0 +1,9 @@
+{% extends "modal.html" %}
+{% load i18n %}
+{% block header %}
+ {% trans "Order History" %}
+{% endblock header %}
+
+{% block body %}
+ {% include "orders/list.html" %}
+{% endblock body %}
diff --git a/servo/templates/orders/index.html b/servo/templates/orders/index.html
new file mode 100755
index 0000000..21116d5
--- /dev/null
+++ b/servo/templates/orders/index.html
@@ -0,0 +1,55 @@
+{% extends "default.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+{% if perms.servo.add_order %}
+ <a href="{% url 'orders-create' %}" class="btn"><i class="icon-plus"></i> {% trans "Create Order" %}</a>
+ {% if request.session.order_queryset %}
+ <a href="{% url 'orders-download_results' %}" class="btn"><i class="icon-download"></i> {% trans "Download Results" %}</a>
+ {% endif %}
+{% else %}
+ <a href="#" class="btn disabled"><i class="icon-plus"></i> {% trans "Create Order" %}</a>
+{% endif %}
+{% endblock toolbar %}
+
+{% block content %}
+<div class="page-header clearfix">
+ <div class="pull-left">
+ <h2><i class="glyphicons-icon inbox_in"></i> {{ title }}<br/><small style="margin-left:60px">{{ subtitle }}</small></h2>
+ </div>
+ <form class="form-search pull-right">
+ <div class="input-append">
+ <input type="text" class="search-query filter" placeholder="{% trans "Filter results" %}"/>
+ <button type="button" class="btn" data-toggle="collapse" data-target="#collapsable"><i class="icon-search"></i></button>
+ </div>
+ </form>
+</div>
+
+<div id="collapsable" class="collapse out">
+ <div class="well clearfix">
+ <form action="" method="get" class="form-inline">
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <p class="clearfix"><hr/></p>
+ <div class="pull-right">
+ <a class="btn" href="{% url 'orders-index' %}?state=0"><i class="icon-refresh"></i> {% trans "Reset" %}</a>
+ <button class="btn btn-primary" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </form>
+ </div>
+</div>
+
+{% block second_column %}
+{% include "orders/list.html" %}
+{% endblock second_column %}
+
+{% endblock content %}
+
+{% block crumbs %}
+<li><a href="{% url 'orders-index' %}">{% trans "Orders" %}</a></li>
+{% endblock crumbs %}
diff --git a/servo/templates/orders/list.html b/servo/templates/orders/list.html
new file mode 100755
index 0000000..2e5347b
--- /dev/null
+++ b/servo/templates/orders/list.html
@@ -0,0 +1,43 @@
+{% load staticfiles %}
+{% load servo_tags %}
+{% load humanize %}
+{% load cache %}
+{% load i18n %}
+
+<table class="table table-hover sortable">
+ <thead>
+ <tr>
+ <th style="width:60px">{% trans "Order" %}</th>
+ <th>{% trans "Customer" %}</th>
+ <th>{% trans "Created" %}</th>
+ <th>{% trans "Assigned to" %}</th>
+ <th>{% trans "Status" %}</th>
+ <th data-defaultsort="disabled"></th>
+ </tr>
+ </thead>
+ <tbody class="searchable">
+ {% for order in orders %}
+ <tr>
+ <td><a href="{{ order.get_absolute_url }}">{{ order.code }}</a></td>
+ <td data-value="{{ order.customer_name }}">{% if order.customer_name %}<strong>{{ order.customer_name }}</strong>{% else %}<span class="muted">{% trans "No customer" %}</span>{% endif %}<br/>{% if order.description %}{{ order.description }}{% else %}<span class="muted">{% trans "No description" %}</span>{% endif %} {% cache 120 order_tags order.pk %} {% for t in order.tags.all %}<span class="label label-{{ t.color }}">{{ t.title }}</span>&nbsp;{% endfor %}{% endcache %}</td>
+ <td data-value="{{ order.created_at|date:"U" }}">{{ order.created_at|naturaltime }}<br/><small class="muted">{{ order.created_at|date:"SHORT_DATETIME_FORMAT" }}</small></td>
+ <td>
+ {% if order.user %}
+ {{ order.get_user_name }}<br/>
+ <small class="muted">{{ order.started_at|naturaltime|default:"" }}</small>
+ {% else %}
+ <span class="muted">{% trans "Nobody" %}</span>
+ {% endif %}
+ <td data-value="{{ order.status_name }}">{% if order.status_name %}{{ order.status_name }}{% else %}<span class="muted">{% trans "No status" %}</span>{% endif %}<br/><small class="muted">{{ order.status_started_at|naturaltime|default:"" }}</small></td>
+ <td><img src="{% static order.get_status_img %}" title="{{ order.status_name }}" alt="{{ order.status_name }}" class="status_color"/></td>
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="6" class="empty muted">{% trans "No orders found" %}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% if orders.paginator %}
+ {% include "pagination.html" with items=orders %}
+ {% endif %}
diff --git a/servo/templates/orders/list_products.html b/servo/templates/orders/list_products.html
new file mode 100755
index 0000000..051c9fe
--- /dev/null
+++ b/servo/templates/orders/list_products.html
@@ -0,0 +1,22 @@
+{% load i18n %}
+{% load servo_tags %}
+
+<div id="products">
+ <ul class="unstyled" data-url="{% url 'orders-list_products' order.pk %}">
+ {% for p in order.products %}
+ <li>
+ <small><strong>{{ p.code }}</strong></small><small class="pull-right">{{ p.price|currency }}</small>
+ <br/>
+ <small class="muted">{{ p.title }} - {{ p.get_price_category_display }}</small><br/>
+ {% if p.kbb_sn %}
+ <i class="icon-barcode"></i> <a href="#" class="sn" title="{% trans 'Click to select EEE code' %}">{{ p.kbb_sn }}</a>
+ {% endif %}
+ </li>
+ {% empty %}
+ <li class="muted text-center" style="padding:10px">{% trans "No products added" %}</li>
+ {% endfor %}
+ </ul>
+ <hr/>
+ <strong>{% trans "Total" %}:</strong>
+ <span class="pull-right"><strong>{{ order.gross_total|currency }}</strong></span>
+</div>
diff --git a/servo/templates/orders/notes.html b/servo/templates/orders/notes.html
new file mode 100755
index 0000000..06cc61d
--- /dev/null
+++ b/servo/templates/orders/notes.html
@@ -0,0 +1,84 @@
+{% load i18n %}
+{% load humanize %}
+{% load mptt_tags %}
+{% load servo_tags %}
+
+{% if not order.notes %}
+<div class="row-fluid">
+ <div class="span12 well empty"><h3 class="muted">{% trans "Order doesn't contain any notes." %}</h3></div>
+</div>
+{% endif %}
+
+<ul class="media-list" style="margin-bottom:0px">
+{% recursetree notes %}
+ {% if node.is_root_node %}<li {% else %}<div {% endif %}class="media">
+ <a class="pull-left" href="#">
+ {% with node.created_by as profile %}
+ <img src="{{ profile.get_avatar }}" alt="{{ profile }}" title="{{ profile }}" class="avatar"/>
+ {% endwith %}
+ </a>
+ <div class="btn-group pull-right">
+ <a href="{% url 'notes-reply' node.pk %}" class="btn btn-small" title="{% trans "Reply" %}"><i class="icon-share-alt"></i></a>
+ {% if perms.servo.change_note and order.is_editable %}
+ <a href="{{ node.get_edit_url }}" class="btn btn-small"><i class="icon-pencil"></i></a>
+ {% else %}
+ <a href="#" class="btn btn-small disabled"><i class="icon-pencil"></i></a>
+ {% endif %}
+ <a class="btn btn-small dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li class="dropdown-submenu pull-left">
+ <a tabindex="-1" href="#"><i class="icon-tag"></i> {% trans "Tag" %}</a>
+ <ul class="dropdown-menu">
+ {% for t in note_tags %}
+ <li><a href="{% url 'notes-toggle_tag' node.pk t.pk %}">{{ t }}</a></li>
+ {% endfor %}
+ </ul>
+ </li>
+ <li class="dropdown-submenu pull-left">
+ <a tabindex="-1" href="#"><i class="icon-flag"></i> {% trans "Mark" %}</a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'notes-toggle_flag' node.pk 'reported' %}" class="nofollow">{{ node.get_reported_title }}</a></li>
+ <li><a href="{% url 'notes-toggle_flag' node.pk 'read' %}" class="nofollow">{{ node.get_read_title }}</a></li>
+ <li><a href="{% url 'notes-toggle_flag' node.pk 'flagged' %}" class="nofollow">{{ node.get_flagged_title }}</a></li>
+ </ul>
+ </li>
+ <li{% if not order.is_editable %} class="disabled"{% endif %}><a href="{% url 'notes-copy' node.pk %}"><i class="icon-plus"></i> {% trans "Copy" %}</a></li>
+ <li><a href="{% url 'notes-messages' node.pk %}" data-modal="#modal"><i class="icon-info-sign"></i> {% trans "Message Log" %}</a></li>
+ <li class="divider"></li>
+ <li{% if not order.is_editable %} class="disabled"{% endif %}><a href="{% url 'notes-delete_note' node.pk %}" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ </ul>
+ </div>
+ <div class="media-body">
+ <h5 class="media-heading">{{ node.get_sender_name }} {{ node.created_at|relative_date }}
+ {% if node.escalation.is_submitted %}
+ <small class="muted"><i class="icon-globe"></i> {{ node.escalation.escalation_id }}</small>
+ {% endif %}
+ </h5>
+ {% if node.is_reported %}
+ {{ node.body|markdown }}
+ {% else %}
+ <em>{{ node.body|markdown }}</em>
+ {% endif %}
+ {% if node.message_set.all|length %}
+ <i class="icon-envelope" title="{% trans "Note has been sent" %}"></i>
+ {% endif %}
+ {% with node.attachments.all as attachments %}
+ {% for a in attachments %}
+ <a class="window" href="{{ a.content.url }}"><span class="label label-info"><i class="icon-file icon-white"></i> {{ a }}</span></a>
+ {% endfor %}
+ {% endwith %}
+ {% if not node.is_leaf_node %}
+ {{ children }}
+ {% endif %}
+ {% for t in node.labels.all %}
+ <span class="label label-{{ t.color }}">{{ t.title }}</span>
+ {% endfor %}
+ </div>
+ {% if node.is_root_node %}</li><hr/>{% else %}</div>{% endif %}
+{% endrecursetree %}
+</ul>
+<p class="clearfix">
+ <a href="{% url 'orders-add_note' order.pk %}" class="btn pull-right {% if order.is_closed %}disabled{% endif %}"><i class="icon-plus"></i> {% trans "Add Note" %}</a>
+</p>
diff --git a/servo/templates/orders/parts.html b/servo/templates/orders/parts.html
new file mode 100755
index 0000000..bf3cbf3
--- /dev/null
+++ b/servo/templates/orders/parts.html
@@ -0,0 +1,36 @@
+{% extends "orders/edit.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+{% endblock toolbar %}
+
+{% block first_column %}
+<address>
+ <strong>{{ device.description }}</strong><br/>
+ <small class="muted">{{ device.configuration }}</small><br/>
+ <span><a href="{% url 'devices-get_info' device.pk %}" data-modal="#modal">{{ device.get_warranty_status_display }}</a></span> <i class="icon-info-sign"></i><br/>
+ <i class="icon-barcode"></i> <a href="#" class="sn">{{ device.sn }}</a>
+ <hr/>
+ <strong>{% trans "Parts and Services" %}</strong>
+ {% include "orders/list_products.html" %}
+</address>
+<hr/>
+<input type="text" class="span12 search-query filter" placeholder="{% trans "Filter results" %}"/>
+<a class="btn btn-primary pull-right" style="margin:10px" href="{{ order.get_absolute_url }}">{% trans "Done" %}</a>
+{% endblock first_column %}
+
+{% block second_toolbar %}
+{% endblock second_toolbar %}
+
+{% block second_column %}
+
+<div class="async" data-url="{{ url }}"><h2 class="text-center muted">{% trans "Fetching parts..." %}</h2></div>
+
+{% endblock second_column %}
+
+{% block morecrumbs %}
+<li><a href="{{ order.get_absolute_url }}">{% trans "Order" %} {{ order.get_footer }}</a> <span class="divider">/</span></li>
+<li><a href="">{{ device.description }}</a> <span class="divider">/</span></li>
+<li class="active">{% trans "Service Parts" %}</li>
+{% endblock morecrumbs %} \ No newline at end of file
diff --git a/servo/templates/orders/print_confirmation.html b/servo/templates/orders/print_confirmation.html
new file mode 100755
index 0000000..0b0ca77
--- /dev/null
+++ b/servo/templates/orders/print_confirmation.html
@@ -0,0 +1,193 @@
+{% extends "default_print.html" %}
+{% load servo_tags %}
+{% load mptt_tags %}
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+{% with order.location as location %}
+<div class="row">
+ <div class="span4">
+ {% if location.logo %}
+ <img src="{% get_media_prefix %}{{ conf.company_logo }}" alt="logo" title="logo" class="media-object pull-left span2" style="margin:10px"/>
+ {% endif %}
+ </div>
+ <div class="span4">
+ {{ location.title }}<br/>
+ {{ location.address }}<br/>
+ {{ location.zip_code }}, {{ location.city }}<br/>
+ {{ location.notes }}
+ </div>
+ <div class="span4">
+ {% block location_info %}
+ {% endblock location_info %}
+ {% trans "Email Address" %}: {{ location.email }}<br/>
+ {% trans "Phone" %}: {{ location.phone }}<br/><br/>
+ {% trans "Repair Number" %}: {{ order.code }}<br/>
+ <img src="{% url 'barcodes-view' order.code %}" alt="{{ order.code }}"/>
+ </div>
+</div>
+{% endwith %}
+<div class="row" style="margin-top:2em;margin-bottom:1em">
+ <div class="span6">
+ {% trans "Date" %}: {% now "SHORT_DATE_FORMAT" %}
+ </div>
+</div>
+<div class="row">
+ <div class="span12 text-center"><h3>{% block title %}{% trans "Work Confirmation" %} {{ order.code }}{% endblock title %}</h3></div>
+</div>
+<div class="row">
+ <div class="span12">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Customer Information" %}</th>
+ <th>{% trans "Product Information" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ {% block customer_details %}
+ <strong>{{ order.customer.fullname }}</strong><br/>
+ {% trans "Daytime Phone" %}: {{ order.customer.phone|default:"-" }}<br/>
+ {% trans "Email Address" %}: {{ order.customer.email|default:"-" }}
+ {% if order.customer.street_address %}
+ <br/>
+ {% trans "Address" %}: {{ order.customer.street_address }}<br/>
+ {{ order.customer.zip_code }} {{ order.customer.city }}
+ {% endif %}
+ {% endblock customer_details %}
+ </td>
+ <td>
+ {% for device in order.get_devices %}
+ {% trans "Model" %}: {{ device.description }}<br/>
+ {% trans "Warranty Status" %}: {{ device.get_warranty_status_display }}<br/>
+ {% trans "Estimated Date of Purchase" %}: {{ device.purchased_on|default:"-" }}<br/>
+ {% if device.imei %}
+ {% trans "IMEI Number" %}: {{ device.imei }}<br/>
+ {% trans "Activation Policy" %}: {{ device.initial_activation_policy }}<br/>
+ {% endif %}
+ {% if device.sn %}
+ {% trans "Serial Number" %}: {{ device.sn }}<br/>
+ <img src="{% url 'barcodes-view' device.sn %}" alt=""/>
+ {% endif %}
+ {% endfor %}
+ </td>
+ </tr>
+ {% if order.has_accessories %}
+ <tr><td colspan="2"><strong>{% trans "Accessories" %}:</strong> {{ order.get_accessories|join:", " }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+ </div>
+</div>
+<div class="row">
+ <div class="span12">
+ {% block work_description %}
+ {% if order.reported_notes.count > 0 %}
+ <h4>{% trans "Work Description" %}</h4>
+ {% for n in order.reported_notes %}
+ {{ n.body|markdown }}
+ {% endfor %}
+ {% endif %}
+ {% endblock work_description %}
+ </div>
+</div>
+<div class="row">
+ <div class="span12">
+ {% block parts_and_services %}
+ {% if order.has_products %}
+ <h4>{% trans "Products and Services" %}</h4>
+ <table class="table">
+ <thead>
+ <tr>
+ <th colspan="2">{% trans "Product" %}</th>
+ <th>{% trans "Warranty" %}</th>
+ <th style="width:50px">{% trans "VAT %" %}</th>
+ <th style="width:40px">{% trans "Qty" %}</th>
+ <th style="width:60px">{% trans "0% VAT" %}</th>
+ <th style="width:60px">{% trans "Price" %}</th>
+ <th style="width:60px">{% trans "Total" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% block list_products %}
+ {% for i in order.products %}
+ <tr>
+ <td style="width:80px">{{ i.code }}</td>
+ <td>{{ i.title }}{% if i.sn %}<br/><span class="muted">{% trans "SN" %}: {{ i.sn }} {% if i.imei %}
+ {% trans "IMEI" %}: {{ i.imei }}{% endif %}{% endif %}</span></td>
+ <td>{{ i.product.get_warranty_display|default:"" }}</td>
+ <td>{{ i.product.pct_vat|floatformat:"0" }}%</td>
+ <td>{{ i.amount }}</td>
+ <td>{{ i.price_notax|currency }}</td>
+ <td>{{ i.price|currency }}</td>
+ <td>{{ i.total_gross|currency }}</td>
+ </tr>
+ {% endfor %}
+ {% endblock list_products %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td style="text-align:right">{% trans "Total" %}:</td>
+ <td>{% block order_total %}{{ order.gross_total|currency }}{% endblock order_total %}</td>
+ </tr>
+ </tfoot>
+ </table>
+ {% endif %}
+ {% endblock parts_and_services %}
+ </div>
+</div>
+<div class="row">
+ <div class="span12">
+ {% block customer_pickup %}
+ <h4>{% trans "Customer Pickup" %}</h4>
+ <table class="table">
+ <tbody>
+ <tr>
+ <td style="width:40%">&nbsp;</td>
+ <td style="width:40%">&nbsp;</td>
+ <td style="width:20%">&nbsp;</td>
+ </tr>
+ <tr>
+ <td>{% trans "Customer Name" %}</td>
+ <td>{% trans "Signature" %}</td>
+ <td>{% trans "Date" %}</td>
+ </tr>
+ </tbody>
+ </table>
+ {% endblock customer_pickup %}
+ {% block received_by %}
+ <h5>{% trans "Received By" %}</h5>
+ <table class="table">
+ <tbody>
+ <tr>
+ <td style="width:40%">{{ order.created_by.get_full_name }}</td>
+ <td style="width:40%">&nbsp;</td>
+ <td style="width:20%">{{ order.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ </tr>
+ <tr>
+ <td>{% trans "Name" %}</td>
+ <td>{% trans "Signature" %}</td>
+ <td>{% trans "Date" %}</td>
+ </tr>
+ </tbody>
+ </table>
+ {% endblock received_by %}
+ </div>
+</div>
+<div class="row">
+ <div class="span12">
+ <hr/>
+ {% block tos %}{{ conf.terms_of_service|markdown }}{% endblock tos %}
+ {% block footer %}{% endblock footer %}
+ </div>
+</div>
+{% endblock content %}
diff --git a/servo/templates/orders/print_dispatch.html b/servo/templates/orders/print_dispatch.html
new file mode 100755
index 0000000..003e03e
--- /dev/null
+++ b/servo/templates/orders/print_dispatch.html
@@ -0,0 +1,38 @@
+{% extends "orders/print_confirmation.html" %}
+{% load i18n %}
+
+{% block title %}
+ {% blocktrans with code=order.code%}Dispatch #{{ code }}{% endblocktrans %}
+{% endblock title %}
+
+{% block parts_and_services %}
+<h4>{% trans "Products and Services" %}</h4>
+<table class="table">
+ <thead>
+ <tr>
+ <th colspan="2">{% trans "Product" %}</th>
+ <th>{% trans "Warranty" %}</th>
+ <th style="width:50px">{% trans "VAT %" %}</th>
+ <th style="width:40px">{% trans "Qty" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% block list_products %}
+ {% for i in order.products %}
+ <tr>
+ <td style="width:75px">{{ i.product.code }}</td>
+ <td>{{ i.title }}{% if i.sn %}<br/><span class="muted">{% trans "SN" %}: {{ i.sn }}</span>{% endif %}</td>
+ <td>{{ i.product.warranty_period }} {% trans "mon." %}</td>
+ <td>{{ i.product.pct_vat|floatformat:"0" }}%</td>
+ <td>{{ i.amount }}</td>
+ </tr>
+ {% endfor %}
+ {% endblock list_products %}
+ </tbody>
+</table>
+{% endblock parts_and_services %}
+
+{% block received_by %}
+{% endblock received_by %}
+
+{% block tos %}{% endblock tos %}
diff --git a/servo/templates/orders/print_quote.html b/servo/templates/orders/print_quote.html
new file mode 100755
index 0000000..3c3a7d7
--- /dev/null
+++ b/servo/templates/orders/print_quote.html
@@ -0,0 +1,3 @@
+{% extends "orders/print_confirmation.html" %}
+
+{% block tos %}{% endblock tos %}
diff --git a/servo/templates/orders/print_receipt.html b/servo/templates/orders/print_receipt.html
new file mode 100755
index 0000000..c4de827
--- /dev/null
+++ b/servo/templates/orders/print_receipt.html
@@ -0,0 +1,46 @@
+{% extends "orders/print_confirmation.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block title %}
+ {% blocktrans with invoice.pk as number %}Receipt {{ number }}{% endblocktrans %}
+{% endblock title %}
+
+{% block customer_details %}
+ <strong>{{ invoice.customer_name }}</strong><br/>
+ {% trans "Daytime Phone" %}: {{ invoice.customer_phone|default:"-" }}<br/>
+ {% trans "Email Address" %}: {{ invoice.customer_email|default:"-" }}
+ {% if invoice.customer_address %}
+ <br/>
+ {% trans "Address" %}: {{ invoice.customer_address }}
+ {% endif %}
+{% endblock customer_details %}
+
+{% block list_products %}
+{% for i in invoice.invoiceitem_set.all %}
+ <tr>
+ <td style="width:80px">{{ i.code }}</td>
+ <td>{{ i.title }}{% if i.sn %}<br/><span class="muted">{% trans "SN" %}: {{ i.sn }} {% if i.imei %}{% trans "IMEI" %}: {{ i.imei }}{% endif %}{% endif %}</span></td>
+ <td>{{ i.product.get_warranty_display|default:"" }}</td>
+ <td>{{ i.product.pct_vat|floatformat:"0" }}%</td>
+ <td>{{ i.amount }}</td>
+ <td>{{ i.price_notax|currency }}</td>
+ <td>{{ i.price|currency }}</td>
+ <td>{{ i.total_gross|currency }}</td>
+ </tr>
+{% endfor %}
+{% endblock list_products %}
+
+{% block order_total %}
+ {{ invoice.total_gross|currency }}
+{% endblock order_total %}
+
+{% block customer_pickup %}
+<p class="text-right">
+ <strong>{% trans "Date of invoice" %}: {{ invoice.created_at|date:"SHORT_DATE_FORMAT" }}</strong><br/>
+ <strong>{% trans "Payment method" %}: {{ invoice.get_payment_methods|join:", " }}</strong><br/>
+ <strong>{% trans "Sales Person" %}: {{ invoice.created_by.get_full_name }}</strong><br/>
+</p>
+{% endblock customer_pickup %}
+
+{% block tos %}{% endblock tos %}
diff --git a/servo/templates/orders/products.html b/servo/templates/orders/products.html
new file mode 100755
index 0000000..4476ae8
--- /dev/null
+++ b/servo/templates/orders/products.html
@@ -0,0 +1,98 @@
+{% load i18n %}
+{% load servo_tags %}
+
+{% with order.serviceorderitem_set.all as products %}
+<table class="table" style="margin-bottom:0px">
+ {% if products.count %}
+ <thead>
+ <tr>
+ <th style="width:64px"></th>
+ <th>{% trans "Product" %}</th>
+ <th>{% trans "Amount" %}</th>
+ <th>{% trans "Price Category" %}</th>
+ <th>{% trans "Price" %}</th>
+ <th>{% trans "Total" %}</th>
+ <th></th>
+ </tr>
+ </thead>
+ {% endif %}
+ <tbody>
+ {% for item in products %}
+ {% with item.product as product %}
+ <tr>
+ <td><img src="{{ product.get_photo }}" alt="{{ product }}" class="img-icon"/></td>
+ <td>
+ <strong><a href="{% url 'products-get_info' item.code order.location.pk %}" data-modal="#modal">{{ product.code }}</a></strong><br/>
+ <div>{{ item.title }}</div>
+ {% if item.sn or item.kbb_sn %}
+ <p><small class="muted">{% trans "Serial Number" %}: {{ item.sn }}{% if item.kbb_sn %}, KBB: {{ item.kbb_sn }}{% endif %}</small></p>
+ {% endif %}
+ {% if product.is_apple_part %}
+ {% for repair in order.get_repairs %}
+ <div class="btn-group">
+ <a class="btn btn-small dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-wrench"></i> {{ repair.confirmation }} <span class="caret"></span>
+ </a>
+ {% if item in repair.get_products %}
+ {% with item.get_part as part %}
+ {% include "repairs/part_menu.html" %}
+ {% endwith %}
+ {% else %}
+ <ul class="dropdown-menu">
+ {% if repair.is_open %}
+ <li><a href="{% url 'repairs-add_part' repair.pk item.pk %}" data-modal="#modal" class="spin">{% trans "Add to Repair" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Add to Repair" %}</a></li>
+ {% endif %}
+ </ul>
+ {% endif %}
+ </div>
+ {% endfor %}
+ {% endif %}
+ </td>
+ <td>{{ item.amount }}</td>
+ <td>{{ item.get_price_category_display }}</td>
+ <td><a href="#" class="tt" title="{{ item.price_notax|currency }}">{{ item.price|currency }}</a></td>
+ <td><a href="#" class="tt" title="{{ item.total_net|currency }}">{{ item.total_gross|currency }}</a></td>
+ <td style="width:125px">
+ {% if order.is_editable %}
+ <div class="btn-group">
+ {% if item.should_report %}
+ <a href="{% url 'orders-report_product' pk=order.id item_id=item.id %}" title="Toggle report flag" class="btn btn-small btn-success nofollow active" data-toggle="button"><i class="icon-ok"></i></a>
+ {% else %}
+ <a href="{% url 'orders-report_product' pk=order.id item_id=item.id %}" title="Toggle report flag" class="btn btn-small btn-success nofollow" data-toggle="button"><i class="icon-ok icon-white"></i></a>
+ {% endif %}
+ </div>
+ <div class="btn-group">
+ <a href="{% url 'orders-edit_product' pk=order.id item_id=item.id %}" class="btn btn-small" title="{% trans "Edit" %}" data-modal="#modal"><i class="icon-pencil"></i></a>
+ <a class="btn btn-small dropdown-toggle {% if not order.is_editable %}disabled{% endif %}" data-toggle="dropdown" href="#"><i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu pull-right">
+ {% if item.can_create_device %}
+ <li><a href="{% url 'orders-create_device' order.pk item.pk %}">{% trans "Create Device" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Create Device" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a href="{% url 'orders-remove_product' pk=order.id item_id=item.id %}" data-modal="#modal" title="{% trans "Delete" %}"><i class="icon-trash"></i> {% trans "Remove" %}</a></li>
+ </ul>
+ </div>
+ {% endif %}
+ </td>
+ </tr>
+ {% endwith %}
+ {% empty %}
+ <div class="row-fluid">
+ <div class="span12 well empty"><h3 class="muted">{% trans "Order doesn't contain any products." %}</h3></div>
+ </div>
+ {% endfor %}
+ </tbody>
+</table>
+<a href="{% url 'products-choose' order_id=order.pk %}" data-modal="#modal" class="btn {% if not order.is_editable %}disabled{% endif %} pull-right"><i class="icon-plus"></i> {% trans "Add Product" %}</a>
+{% endwith %}
+<p class="clearfix">
+ <hr/>
+ {% if order.serviceorderitem_set.count %}
+ <h3 class="pull-right">{% trans "Order Total" %}: <a href="#" class="tt" title="{{ order.net_total|currency }}">{{ order.gross_total|currency }}</a></h3>
+ {% endif %}
+</p>
diff --git a/servo/templates/orders/remove_customer.html b/servo/templates/orders/remove_customer.html
new file mode 100755
index 0000000..53572e2
--- /dev/null
+++ b/servo/templates/orders/remove_customer.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Remove this customer from the order?" %}
+{% endblock header %}
+
+{% block body %}
+ {% trans "This will not delete the customer from the database." %}
+{% endblock body %}
+
+{% block footer %}
+ <form method="post" action="{{ action }}">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Remove" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/orders/remove_device.html b/servo/templates/orders/remove_device.html
new file mode 100755
index 0000000..dcfd376
--- /dev/null
+++ b/servo/templates/orders/remove_device.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with dev=device.description %}Remove {{ dev }}?{% endblocktrans %}
+{% endblock header %}
+
+{% block body %}
+ <p>{% trans "This will not delete the device from the database." %}</p>
+{% endblock body %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger pull-right">{% trans "Remove" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/orders/remove_product.html b/servo/templates/orders/remove_product.html
new file mode 100755
index 0000000..e6d2966
--- /dev/null
+++ b/servo/templates/orders/remove_product.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Really remove product from order?" %}
+{% endblock header %}
+
+{% block body %}
+ {% trans "Product will not be deleted from the warehouse." %}
+{% endblock body %}
+
+{% block footer %}
+<form action="{% url 'servo.views.order.remove_product' order.id item.id %}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+</form>
+{% endblock footer %}
diff --git a/servo/templates/orders/repair.html b/servo/templates/orders/repair.html
new file mode 100755
index 0000000..b3798db
--- /dev/null
+++ b/servo/templates/orders/repair.html
@@ -0,0 +1,63 @@
+{% extends "orders/edit.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+{% if repair.is_open %}
+<a href="{% url 'repairs-complete_repair' order.pk repair.pk %}" class="btn" data-modal="#modal"><i class="icon-ok"></i> {% trans "Complete Repair" %}</a>
+{% else %}
+<a href="#" class="btn disabled" data-modal="#modal"><i class="icon-ok"></i> {% trans "Complete Repair" %}</a>
+{% endif %}
+<a class="btn" href="{% url 'repairs-copy_repair' repair.pk %}"><i class="icon-plus-sign"></i> {% trans "Copy" %}</a>
+{% endblock toolbar %}
+
+{% block second_column %}
+<h3>{{ status }}</h3>
+<dl class="dl-horizontal">
+ <dt>{% trans "Symptom" %}</dt>
+ <dd>{{ repair.symptom }}</dd>
+ <dt>{% trans "Diagnosis" %}</dt>
+ <dd>{{ repair.diagnosis }}</dd>
+ <dt>{% trans "Request Review" %}</dt>
+ <dd>{{ repair.request_review|yesno }}</dd>
+{% if repair.attachment %}
+ <dt>{% trans "Attachment" %}</dt>
+ {% with repair.attachment as a %}
+ <dd><a class="label label-info window" href="{{ a.url }}"><i class="icon-download icon-white"></i> {{ a }}</a></dd>
+ {% endwith %}
+{% endif %}
+ <dt>{% trans "Confirmation" %}</dt>
+ <dd>{{ repair.confirmation }}</dd>
+ <dt>{% trans "Reference" %}</dt>
+ <dd>{{ repair.reference }}</dd>
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ notes|default:"-"|linebreaks }}</dd>
+</dl>
+<table class="table">
+ <tbody>
+ {% for part in parts %}
+ <tr>
+ <td>
+ <a href="{% url 'products-view_product' pk=part.order_item.product_id %}"><strong>{{ part.part_number }}</strong></a>
+ <br/>
+ {{ part.part_title }}<br/>
+ <small>{{ part.get_symptom_code_display }} - {{ part.get_symptom_modifier_display }}</small>
+ </td>
+ <td><br/>{{ part.coverage_description|default:"-" }}</td>
+ {% if part.order_status_code == 'OSHP' %}
+ <td><br/><a href="{{ part.carrier_url }}" class="window">{{ part.order_status }}</a></td>
+ {% else %}
+ <td><br/>{{ part.order_status|default:"-" }}</td>
+ {% endif %}
+ <td style="vertical-align:middle">
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ {% include "repairs/part_menu.html" %}
+ </div>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock second_column %}
diff --git a/servo/templates/orders/reserve_products.html b/servo/templates/orders/reserve_products.html
new file mode 100755
index 0000000..318a28d
--- /dev/null
+++ b/servo/templates/orders/reserve_products.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with id=order.code %}Reserve all products in order {{ id }}?{% endblocktrans %}
+{% endblock header %}
+
+{% block footer %}
+<form action="{{ action }}" method="post" accept-charset="utf-8">
+ {% csrf_token %}
+ <button class="btn btn-primary" type="submit">{% trans "Reserve" %}</button>
+</form>
+{% endblock footer %}
diff --git a/servo/templates/orders/search.html b/servo/templates/orders/search.html
new file mode 100755
index 0000000..2789b20
--- /dev/null
+++ b/servo/templates/orders/search.html
@@ -0,0 +1,7 @@
+{% load i18n %}
+
+{% if orders %}
+ {% include "orders/list.html" %}
+{% else %}
+ <h1 class="muted text-center">{% trans "No orders found" %}</h1>
+{% endif %}
diff --git a/servo/templates/orders/statuses.html b/servo/templates/orders/statuses.html
new file mode 100755
index 0000000..8f3eb4b
--- /dev/null
+++ b/servo/templates/orders/statuses.html
@@ -0,0 +1,7 @@
+{% load i18n %}
+<select name="status" id="select_status">
+ <option value="">{% trans "Choose" %}...</option>
+{% for status in statuses %}
+ <option value="{{ status.id }}">{{ status.title }}</option>
+{% endfor %}
+</select>
diff --git a/servo/templates/orders/tabs.html b/servo/templates/orders/tabs.html
new file mode 100755
index 0000000..134aa9f
--- /dev/null
+++ b/servo/templates/orders/tabs.html
@@ -0,0 +1,8 @@
+{% load i18n %}
+{% load humanize %}
+
+<li{% if not repair and active == 'order' %} class="active"{% endif %}><a href="{{ order.get_absolute_url }}">{% trans "Order" %}</a></li>
+{% for r in order.repair_set.all %}
+<li{% if r.pk == repair.pk %} class="active"{% endif %}><a href="{{ r.get_absolute_url }}">{{ r.get_number }}</a></li>
+{% endfor %}
+<li{% if active == 'events' %} class="active"{% endif %}><a href="{{ order.get_absolute_url }}events/">{% trans "Events" %}</a></li>
diff --git a/servo/templates/orders/toggle_flagged.html b/servo/templates/orders/toggle_flagged.html
new file mode 100755
index 0000000..f062776
--- /dev/null
+++ b/servo/templates/orders/toggle_flagged.html
@@ -0,0 +1 @@
+<i class="icon-flag"></i> {{ action }}
diff --git a/servo/templates/orders/toggle_follow.html b/servo/templates/orders/toggle_follow.html
new file mode 100755
index 0000000..2563974
--- /dev/null
+++ b/servo/templates/orders/toggle_follow.html
@@ -0,0 +1 @@
+<i class="icon-eye-{{ icon }}"></i> {{ action }}
diff --git a/servo/templates/orders/toolbar.html b/servo/templates/orders/toolbar.html
new file mode 100755
index 0000000..4bd9fef
--- /dev/null
+++ b/servo/templates/orders/toolbar.html
@@ -0,0 +1,116 @@
+{% load cache %}
+{% load i18n %}
+
+<div class="btn-group">
+{% if order.is_editable %}
+ <a class="btn" href="{% url 'orders-add_note' order.pk %}"><i class="icon-file"></i> {% trans "Add Note" %}</a>
+{% else %}
+ <a class="btn disabled" href="#"><i class="icon-file"></i> {% trans "Add Note" %}</a>
+{% endif %}
+ <a class="btn dropdown-toggle {% if not order.is_editable %}disabled{% endif %}" data-toggle="dropdown" href="#">
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'devices-choose' order.pk %}" data-modal="#modal">{% trans "Add Device" %}...</a></li>
+ <li><a href="{% url 'products-choose' order_id=order.pk %}" data-modal="#modal">{% trans "Add Product" %}...</a></li>
+ <li><a href="{% url 'orders-choose_customer' order.pk %}" data-modal="#modal">{% trans "Choose Customer" %}...</a></li>
+ </ul>
+</div>
+<div class="btn-group">
+ <a href="{% url 'orders-print_order' order.pk 'confirmation' %}" class="btn window" title="{% trans 'Print' %}"><i class="icon-print"></i> {% trans "Print" %}</a>
+ <button class="btn dropdown-toggle" data-toggle="dropdown">
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'orders-print_order' order.pk 'quote' %}" class="window">{% trans "Cost Estimate" %}</a></li>
+ {% if order.invoice_set.count %}
+ <li><a href="{% url 'orders-print_order' order.pk 'receipt' %}" class="window">{% trans "Receipt" %}</a></li>
+ <li><a href="{% url 'orders-print_order' order.pk 'dispatch' %}" class="window">{% trans "Dispatch" context "noun" %}</a></li>
+ {% endif %}
+ </ul>
+</div>
+<div class="btn-group">
+ <a class="btn dropdown-toggle {% if not order.is_editable %}disabled{% endif %}" data-toggle="dropdown" href="#">
+ <i class="icon-inbox"></i> {{ order.get_queue }} <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% for i in queues %}
+ <li{% if i == order.queue %} class="active"{% endif %}><a href="{% url 'orders-update' order.pk 'queue' i.pk %}">{{ i }}</a></li>
+ {% endfor %}
+ <li class="divider"></li>
+ {% if order.queue %}
+ <li><a href="{% url 'orders-update' order.pk 'queue' 0 %}">{% trans "Remove Queue" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Remove Queue" %}</a></li>
+ {% endif %}
+ </ul>
+</div>
+<div class="btn-group">
+ <a class="btn dropdown-toggle {% if not order.queue or not order.is_editable or statuses|length < 1 %}disabled{% endif %}" data-toggle="dropdown" href="#">
+ <i class="icon-tasks"></i> {{ order.get_status|truncatechars:25 }} <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% for i in statuses %}
+ <li{% if i == order.status %} class="active"{% endif %}><a href="{% url 'orders-update' order.pk 'status' i.pk %}">{{ i }}</a></li>
+ {% endfor %}
+ <li class="divider"></li>
+ <li><a href="{% url 'orders-update' order.pk 'status' 0 %}">{% trans "Remove Status" %}</a></li>
+ </ul>
+</div>
+<div class="btn-group">
+{% if order.can_dispatch %}
+ <a class="btn" href="{% url 'orders-dispatch' order.pk %}">{% trans "Dispatch" %}</a>
+{% else %}
+ <a class="btn disabled" href="#">{% trans "Dispatch" %}</a>
+{% endif %}
+{% if perms.servo.change_order and order.can_close %}
+ <a class="btn" href="{% url 'orders-close' order.pk %}" data-modal="#modal"><i class="icon-lock"></i> {% trans "Close" %}</a>
+{% elif perms.servo.delete_order and order.is_closed %}
+ <a class="btn" href="{% url 'orders-reopen_order' order.pk %}"><i class="icon-lock"></i> {% trans "Reopen" %}</a>
+{% else %}
+ <a class="btn disabled" href="#"><i class="icon-lock"></i> {% trans "Close" %}</a>
+{% endif %}
+</div>
+<div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if request.user in order.followed_by.all %}
+ <li><a href="{% url 'orders-toggle_follow' order.pk %}">{% trans "Unfollow Order" %}</a></li>
+ {% else %}
+ <li><a href="{% url 'orders-toggle_follow' order.pk %}">{% trans "Follow Order" %}</a></li>
+ {% endif %}
+ {% if perms.servo.add_order and perms.servo.copy_order %}
+ <li><a href="{% url 'orders-copy_order' order.pk %}">{% trans "Copy Order" %}</a></li>
+ {% else %}
+ <li><a class="disabled" href="#">{% trans "Copy Order" %}</a></li>
+ {% endif %}
+ <li><a href="{% url 'barcodes-view' order.code %}?f=svg" target="_blank">{% trans "Show Barcode" %}</a></li>
+ <li class="divider"></li>
+ <li class="dropdown-submenu">
+ <a tabindex="-1" href="#">{% trans "Priority" %}</a>
+ <ul class="dropdown-menu">
+ {% for i in priorities %}
+ <li{% ifequal i.0 order.priority %} class="active"{% endifequal %}><a href="{% url 'orders-update' order.pk 'priority' i.0 %}">{{ i.1 }}</a></li>
+ {% endfor %}
+ </ul>
+ </li>
+ <li class="divider"></li>
+ {% if order.can_order_products %}
+ <li><a href="{% url 'orders-reserve_products' order.pk %}" data-modal="#modal">{% trans "Reserve Products" %}</a></li>
+ <li><a href="{% url 'purchases-create_po' order_id=order.pk %}">{% trans "Order Products" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Reserve Products" %}</a></li>
+ <li class="disabled"><a href="#">{% trans "Order Products" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a href="{% url 'repairs-import_repair' order.pk %}" data-modal="#modal">{% trans "Import GSX Repair" %}</a></li>
+ <li class="divider"></li>
+ {% if perms.servo.delete_order and order.is_editable %}
+ <li><a href="{% url 'orders-delete_order' order.pk %}" data-modal="#modal"><i class="icon-trash"> </i> {% trans "Delete Order" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#"><i class="icon-trash"></i> {% trans "Delete Order" %}</a></li>
+ {% endif %}
+ </ul>
+</div>
diff --git a/servo/templates/pagination.html b/servo/templates/pagination.html
new file mode 100755
index 0000000..5625b7c
--- /dev/null
+++ b/servo/templates/pagination.html
@@ -0,0 +1,18 @@
+{% load i18n %}
+{% load servo_tags %}
+
+<div class="pagination pagination-centered">
+ <ul>
+ {% if items.has_previous %}
+ <li><a href="?{% paginator_page request items.previous_page_number %}"><span>&laquo;</span></a></li>
+ {% else %}
+ <li class="disabled"><a href="#"><span>&laquo;</span></a></li>
+ {% endif %}
+ <li class="active"><a href="#"><span>{% trans "Page" %} {{ items.number }}/{{ items.paginator.num_pages }}</span></a></li>
+ {% if items.has_next %}
+ <li><a href="?{% paginator_page request items.next_page_number %}"><span>&raquo;</span></a></li>
+ {% else %}
+ <li class="disabled"><a href="#"><span>&raquo;</span></a></li>
+ {% endif %}
+ </ul>
+</div>
diff --git a/servo/templates/products/category_form.html b/servo/templates/products/category_form.html
new file mode 100755
index 0000000..72ec93a
--- /dev/null
+++ b/servo/templates/products/category_form.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Edit Product Group" %}
+{% endblock header %}
+
+{% block body %}
+ <form action="{{ request.path }}" method="post" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ </form>
+{% endblock body %}
diff --git a/servo/templates/products/choose-list.html b/servo/templates/products/choose-list.html
new file mode 100755
index 0000000..3188808
--- /dev/null
+++ b/servo/templates/products/choose-list.html
@@ -0,0 +1,9 @@
+{% load i18n %}
+{% load servo_tags %}
+<ul class="nav nav-pills nav-stacked">
+{% for i in products %}
+ <li><a href="{% url target_url order i.pk %}"><strong>{{ i.code}}</strong><br/>{{ i.title }}<span class="pull-right">{{ i.price_sales_stock|currency }}</span></a></li>
+{% empty %}
+ <li style="text-align:center">{% trans "No products found" %}</li>
+{% endfor %}
+</ul>
diff --git a/servo/templates/products/choose.html b/servo/templates/products/choose.html
new file mode 100755
index 0000000..a34834b
--- /dev/null
+++ b/servo/templates/products/choose.html
@@ -0,0 +1,25 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Search for a product" %}
+{% endblock header %}
+
+{% block body %}
+ <form method="post" action="{{ action }}" accept-charset="utf-8" data-target="#search-results" id="search-form">
+ {% csrf_token %}
+ <input type="text" style="width:500px" class="search-query" name="q" autocomplete="off" placeholder="{% trans "Code, title or category" %}"/>
+ </form>
+ <div id="search-results"></div>
+{% endblock body %}
+
+{% block footer %}
+ <button class="btn" data-dismiss="modal" aria-hidden="true">{% trans "Close" %}</button>
+ <a class="btn btn-primary submit-search" href="#">{% trans "Search" %}</a>
+ <script type="text/javascript">
+ $('.submit-search').click(function(e){
+ $('#search-form').submit();
+ e.preventDefault();
+ });
+ </script>
+{% endblock footer %} \ No newline at end of file
diff --git a/servo/templates/products/delete_category.html b/servo/templates/products/delete_category.html
new file mode 100755
index 0000000..e9aed7b
--- /dev/null
+++ b/servo/templates/products/delete_category.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Delete product category?" %}
+{% endblock header %}
+
+{% block body %}
+ {% trans "This action will not delete the products in this category." %}
+{% endblock body %}
+
+{% block footer %}
+ <form method="post" action="{{ action }}">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/products/form.html b/servo/templates/products/form.html
new file mode 100755
index 0000000..0429e6a
--- /dev/null
+++ b/servo/templates/products/form.html
@@ -0,0 +1,90 @@
+{% extends "products/view.html" %}
+{% load i18n %}
+
+{% block third_column %}
+<div class="span6">
+ <form method="post" action="" class="form-horizontal" enctype="multipart/form-data">
+ {% csrf_token %}
+ {{ formset.management_form }}
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "General" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Exchange Price" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "Stock Price" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "Stock" %}</a></li>
+ <li><a href="#tab5" data-toggle="tab">{% trans "Details" %}</a></li>
+ <li><a href="#tab6" data-toggle="tab">{% trans "GSX" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ {% include "form_field_snippet.html" with field=form.code %}
+ {% include "form_field_snippet.html" with field=form.title %}
+ {% include "form_field_snippet.html" with field=form.description %}
+ {% include "form_field_snippet.html" with field=form.is_service %}
+ </div>
+ <div class="tab-pane calculate" id="tab2">
+ {% include "form_field_snippet.html" with field=form.price_purchase_exchange %}
+ {% include "form_field_snippet.html" with field=form.pct_margin_exchange %}
+ {% include "form_field_snippet.html" with field=form.price_notax_exchange %}
+ {% include "form_field_snippet.html" with field=form.price_sales_exchange %}
+ </div>
+ <div class="tab-pane calculate" id="tab3">
+ {% include "form_field_snippet.html" with field=form.price_purchase_stock %}
+ {% include "form_field_snippet.html" with field=form.pct_margin_stock %}
+ {% include "form_field_snippet.html" with field=form.price_notax_stock %}
+ {% include "form_field_snippet.html" with field=form.price_sales_stock %}
+ </div>
+ <div class="tab-pane" id="tab4">
+ {% for f in formset %}
+ <div class="table">
+ {% include "form_snippet.html" with form=f %}
+ </div>
+ {% endfor %}
+ <a class="btn" id="add_more"><i class="icon-plus"></i> {% trans "Add" %}</a>
+ </div>
+ <div class="tab-pane" id="tab5">
+ {% include "form_field_snippet.html" with field=form.pct_vat %}
+ {% include "form_field_snippet.html" with field=form.shipping %}
+ {% include "form_field_snippet.html" with field=form.warranty_period %}
+ {% include "form_field_snippet.html" with field=form.brand %}
+ {% include "form_field_snippet.html" with field=form.categories %}
+ <div class="control-group">
+ <label class="control-label">{% trans "Tags" %}</label>
+ <div class="controls">
+ <input type="text" name="tag" class="input typeahead" placeholder="{% trans "Enter tag" %}" data-provide="typeahead" data-source="{% url 'products-tags' %}" autocomplete="off"/>
+ <div style="margin-top:10px">
+ {% for t in product.tags.all %}
+ <span class="label"><div class="pull-left">{{ t.tag }}</div><a class="close" href="{% url 'tags-clear' t.pk %}"><i class="icon-remove icon-white"></i></a></span>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ {% include "form_field_snippet.html" with field=form.photo %}
+ {% include "form_field_snippet.html" with field=form.fixed_price %}
+ {% include "form_field_snippet.html" with field=form.is_serialized %}
+ </div>
+ <div class="tab-pane" id="tab6">
+ {% include "form_field_snippet.html" with field=form.part_type %}
+ {% include "form_field_snippet.html" with field=form.labour_tier %}
+ {% include "form_field_snippet.html" with field=form.component_code %}
+ {% include "form_field_snippet.html" with field=form.eee_code %}
+ </div>
+ </div>
+ <div class="form-actions">
+ {% if product.pk %}
+ <a class="btn" href="{% url 'products-view_product' product.pk %}">{% trans "Cancel" %}</a>
+ {% else %}
+ <a class="btn" href="{% url 'products-list_products' %}">{% trans "Cancel" %}</a>
+ {% endif %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ </div>
+ </form>
+</div>
+{% endblock third_column %}
+
+{% block media %}
+<script type="text/javascript">
+ $('#add_more').click(function() {
+ cloneMore('div.table:last', 'inventory_set');
+ });
+</script>
+{% endblock media %}
diff --git a/servo/templates/products/get_info.html b/servo/templates/products/get_info.html
new file mode 100755
index 0000000..be024e2
--- /dev/null
+++ b/servo/templates/products/get_info.html
@@ -0,0 +1,59 @@
+{% extends "modal.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Product Info" %}
+{% endblock header %}
+
+{% block body %}
+<div class="media">
+ <a class="pull-left" href="#">
+ <img class="media-object" src="{{ product.get_photo }}" data-src="{{ product.get_photo }}" width="64px"/>
+ </a>
+ <div class="media-body">
+ <h4 class="media-heading">{{ product.code }}</h4>
+ <p>{{ product.title }}</p>
+ <p>{{ product.description|default:"" }}</p>
+ </div>
+</div>
+<dl class="dl-horizontal">
+ <dt>{% trans "Stock Price" %}</dt>
+ <dd>{{ product.price_sales_stock|currency }}</dd>
+ <dt>{% trans "Exchange Price" %}</dt>
+ <dd>{{ product.price_sales_exchange|currency }}</dd>
+ {% if product.component_code %}
+ <dt>{% trans "Component Group" %}</dt>
+ <dd>{{ product.get_component_code_display }} ({{ product.get_part_type_display }})</dd>
+ {% endif %}
+</dl>
+<dl class="dl-horizontal">
+ <dt>{% trans "EEE Code" %}</dt>
+ <dd>{{ product.eee_code|default:"-"|addspace }}</dd>
+</dl>
+{% for i in inventory %}
+{% ifchanged i.location.id %}
+<hr/>
+<h4>{{ i.location.title }}</h4>
+{% endifchanged %}
+<dl class="dl-horizontal">
+ <dt>{% trans "Stocked" %}</dt>
+ <dd>{{ i.amount_stocked }}</dd>
+ <dt>{% trans "Ordered" %}</dt>
+ <dd>{{ i.amount_ordered|default:"-" }}</dd>
+ <dt>{% trans "Reserved" %}</dt>
+ <dd>{{ i.amount_reserved|default:"-" }}</dd>
+</dl>
+{% empty %}
+ <hr/>
+ <div class="muted text-center">{% trans "Product not in inventory" %}</div>
+{% endfor %}
+{% endblock body %}
+
+{% block footer %}
+{% if product.pk %}
+ <a class="btn btn-default" href="{{ product.get_absolute_url }}">{% trans "View" %}</a>
+ <a class="btn btn-default" href="{% url 'products-edit_product' pk=product.pk group='all' %}">{% trans "Edit" %}</a>
+{% endif %}
+ <button type="submit" class="btn btn-primary" data-dismiss="modal">{% trans "Done" %}</button>
+{% endblock footer %} \ No newline at end of file
diff --git a/servo/templates/products/index.html b/servo/templates/products/index.html
new file mode 100755
index 0000000..fefcecd
--- /dev/null
+++ b/servo/templates/products/index.html
@@ -0,0 +1,104 @@
+{% extends "default.html" %}
+{% load i18n %}
+{% load mptt_tags %}
+{% load servo_tags %}
+
+{% block toolbar %}
+<div class="btn-group">
+ {% if perms.servo.add_product %}
+ <a class="btn" href="{% url 'products-create' group=group.slug %}"><i class="icon-plus"></i> {% trans "New Product" %}</a>
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><span class="caret"></span></a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'products-create_category' %}" data-modal="#modal">{% trans "New Category" %}</a></li>
+ <li class="divider"></li>
+ {% if product.id and perms.servo.add_purchaseorder %}
+ <li><a href="{% url 'orders-create_with_product' product_id=product.id %}">{% trans "New Sales Order" %}</a></li>
+ <li><a href="{% url 'purchases-create_po' product_id=product.id %}">{% trans "New Purchase Order" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="">{% trans "New Sales Order" %}</a></li>
+ <li class="disabled"><a href="">{% trans "New Purchase Order" %}</a></li>
+ {% endif %}
+ </ul>
+ {% endif %}
+</div>
+<div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if group.pk and perms.servo.add_product %}
+ <li><a href="{% url 'products-edit_category' group.slug %}" data-modal="#modal">{% trans "Edit Category" %}</a></li>
+ <li><a href="{% url 'products-delete_category' group.slug %}" data-modal="#modal">{% trans "Delete Category" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Edit Category" %}</a></li>
+ <li class="disabled"><a href="#">{% trans "Delete Category" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a href="{% url 'products-download' %}">{% trans "Download Products" %}</a></li>
+ <li><a href="{% url 'products-upload_products' %}" data-modal="#modal">{% trans "Upload Products" %}</a></li>
+ <li><a href="{% url 'products-upload_gsx_parts' %}" data-modal="#modal">{% trans "Upload Parts Database" %}</a></li>
+ </ul>
+</div>
+{% endblock toolbar %}
+
+{% block content %}
+<div class="row-fluid row-header">
+ <div class="span12">
+ <form class="form-search pull-right">
+ <div class="input-append">
+ <input type="text" class="search-query filter" placeholder="{% trans "Filter results" %}"/>
+ <button type="button" class="btn" data-toggle="collapse" data-target="#collapsable"><i class="icon-search"></i></button>
+ </div>
+ </form>
+ </div>
+</div>
+{% include "products/tabs.html" %}
+<div class="row-fluid">
+ {% block second_column %}
+ <div class="span3">
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Categories" %}</li>
+ <li class="{% active request 'all' %}"><a href="{% url 'products-list_products' %}">{% trans "All" %}</a></li>
+ {% recursetree categories %}
+ <li class="{% active request 'products' node.slug %}">
+ <a href="{% url 'products-list_products' group=node.slug %}">{{ node.title }}</a>
+ {% if not node.is_leaf_node %}
+ <ul class="nav nav-list">{{ children }}</ul>
+ {% endif %}
+ </li>
+ {% endrecursetree %}
+ </ul>
+ </div>
+ <div class="span9">
+ <div id="collapsable" class="collapse out">
+ <form method="post" action="" class="form-inline">
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label|default:"&nbsp;" }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <p class="clearfix"><hr/></p>
+ <div class="pull-right">
+ <button type="submit" class="btn btn-primary"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </form>
+ </div>
+ {% include "products/list.html" %}
+ {% include "pagination.html" with items=products %}
+ </div>
+ {% endblock second_column %}
+
+ {% block third_column %}
+
+ {% endblock third_column %}
+</div>
+{% endblock content %}
+
+{% block crumbs %}
+<li><a href="{% url 'products-list_products' %}">{% trans "Products" %}</a></li>
+{% block breadcrumb %}
+<li class="active"><span class="divider">/</span>{{ group_name }}</li>
+{% endblock breadcrumb %}
+{% endblock crumbs %}
diff --git a/servo/templates/products/index_outgoing.html b/servo/templates/products/index_outgoing.html
new file mode 100755
index 0000000..5b1d007
--- /dev/null
+++ b/servo/templates/products/index_outgoing.html
@@ -0,0 +1,56 @@
+{% extends "products/index.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+
+{% endblock toolbar %}
+
+{% block first_column %}
+<ul class="nav nav-list">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Outgoing" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Shipped" %}</a></li>
+</ul>
+{% endblock first_column %}
+
+{% block second_column %}
+<div class="span9">
+ <form method="post" action="">
+ {% csrf_token %}
+ <table class="table table-hover">
+ <thead>
+ <tr>
+ <th><input type="checkbox" class="toggle_column"/></th>
+ <th>{% trans "Code" %}</th>
+ <th>{% trans "Title" %}</th>
+ <th>{% trans "Order" %}</th>
+ <th>{% trans "Return Order" %}</th>
+ <th>{% trans "Register For Return" %}</th>
+ <th>{% trans "Serial Number" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for p in parts %}
+ <tr>
+ <td><input type="checkbox" name="items" value="{{ p.returnOrderNumber }}"/></td>
+ <td>{{ p.partNumber }}</td>
+ <td>{{ p.partDescription|truncatechars:42 }}</td>
+ <td>{{ p.purchaseOrderNumber }}</td>
+ <td><a href="{% url 'products-return_label' p.partNumber p.returnOrderNumber %}" class="window">{{ p.returnOrderNumber }}</a></td>
+ <td>{{ p.registeredForReturn }}</td>
+ <td>{{ p.kbbSerialNumber }}</td>
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="7" class="muted empty">{% trans "No parts pending return" %}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <button type="submit" class="btn btn-primary pull-right" disabled="disabled">{% trans "Submit" %}</button>
+ </form>
+</div>
+{% endblock second_column %}
+
+{% block breadcrumb %}
+ <li><a href="{% url 'products-outgoing' %}">{% trans "Outgoing" %}</a></li>
+{% endblock breadcrumb %}
diff --git a/servo/templates/products/list.html b/servo/templates/products/list.html
new file mode 100755
index 0000000..bac34ec
--- /dev/null
+++ b/servo/templates/products/list.html
@@ -0,0 +1,27 @@
+{% load i18n %}
+{% load servo_tags %}
+<table class="table table-hover sortable">
+ <thead>
+ <tr>
+ <th style="width:80px" data-defaultsort="disabled"></th>
+ <th>{% trans "Product" %}</th>
+ <th>{% trans "Stock Price" %}</th>
+ <th>{% trans "Exchange Price" %}</th>
+ <th style="width:80px">{% trans "In Stock" %}</th>
+ <th data-defaultsort="disabled"></th>
+ </tr>
+ </thead>
+ <tbody class="searchable">
+ {% include "products/list_rows.html" %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td><strong>{{ total_sales_value|currency }}</strong><br/><strong>{{ total_purchase_value|currency }}</strong></td>
+ <td></td>
+ <td></td>
+ </tr>
+ </tfoot>
+</table>
diff --git a/servo/templates/products/list_rows.html b/servo/templates/products/list_rows.html
new file mode 100755
index 0000000..a753687
--- /dev/null
+++ b/servo/templates/products/list_rows.html
@@ -0,0 +1,55 @@
+{% load i18n %}
+{% load servo_tags %}
+{% for p in products %}
+{% with p.get_pk as product_id %}
+<tr>
+ <td><img src="{{ p.get_photo }}" alt="{{ product }}" class="img-icon"/></td>
+ <td data-value="{{ p.code }}">
+ <a href="{% url 'products-view_product' pk=p.pk group=group.slug %}"><strong>{{ p.code }}</strong></a><br/>
+ {{ p.title }}<br/><small class="muted">{{ p.eee_code|default:""|addspace }}</small>
+ </td>
+ <td data-value="{{ p.price_sales_stock|safe }}">{{ p.price_sales_stock|currency }}</td>
+ <td data-value="{{ p.price_sales_exchange|safe }}">{{ p.price_sales_exchange|currency }}</td>
+ {% if p.is_service %}
+ <td>&infin;</td>
+ {% else %}
+ <td>{{ p|amount_in_location:request.user }}</td>
+ {% endif %}
+ <td>
+ <div class="btn-group pull-right">
+ {% if perms.servo.change_product %}
+ <a class="btn" href="{% url 'products-edit_product' pk=product_id group=group.slug %}"><i class="icon-pencil"></i></a>
+ {% else %}
+ <a class="btn disabled" href="#"><i class="icon-pencil"></i></a>
+ {% endif %}
+ <button class="btn dropdown-toggle" data-toggle="dropdown">
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ {% with request.session.current_order_id as co %}
+ {% if co %}
+ <li><a href="{% url 'orders-add_product' co product_id %}">{% trans "Use in Service Order" %} #{{ request.session.current_order_code }}</a></li>
+ <li class="divider"></li>
+ {% endif %}
+ {% endwith %}
+ <li><a href="{% url 'orders-create_with_product' product_id %}">{% trans "Create Sales Order" %}</a></li>
+ <li><a href="{% url 'purchases-create_po' product_id=product_id %}">{% trans "Create Purchase Order" %}</a></li>
+ {% with request.session.current_po as po %}
+ {% if po %}
+ <li><a href="{% url 'purchases-add_to_po' pk=po.id product_id=product_id %}">{% trans "Use in Purchase Order" %}</a></li>
+ {% endif %}
+ {% endwith %}
+ <li class="divider"></li>
+ {% if perms.servo.delete_product %}
+ <li><a href="{% url 'products-delete_product' pk=product_id group=group.slug %}" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ </td>
+</tr>
+{% endwith %}
+{% empty %}
+<tr><td colspan="6" class="muted empty">{% trans "No products found" %}</td></tr>
+{% endfor %}
diff --git a/servo/templates/products/receive_item.html b/servo/templates/products/receive_item.html
new file mode 100755
index 0000000..275fbd2
--- /dev/null
+++ b/servo/templates/products/receive_item.html
@@ -0,0 +1,51 @@
+{% extends "modal.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block header %}
+ {{ item.code }}
+{% endblock header %}
+
+{% block body %}
+ <form method="post" action="{{ url }}" class="form-horizontal">
+ {% csrf_token %}
+ <h4>{{ item.title }}</h4>
+ <dl class="dl-horizontal">
+ {% with item.purchase_order as po %}
+ <dt>{% trans "Ordered" %}</dt>
+ <dd>{{ po.created_at|date:"SHORT_DATE_FORMAT" }}, {{ po.created_by }}</dd>
+ <dt>{% trans "Service Order" %}</dt>
+ {% if po.sales_order %}
+ <dd><a href="{% url 'orders-edit' po.sales_order.pk %}">{{ po.sales_order.code }}</a></dd>
+ {% else %}
+ <dd>{% trans "Stocking Order" %} {{ po.reference }}</dd>
+ {% endif %}
+ <dt>{% trans "Reference" %}</dt>
+ <dd>{{ po.reference|default:"-" }}</dd>
+ {% if item.received_at %}
+ <dt>{% trans "Received" %}</dt>
+ <dd>{{ item.received_at|date:"SHORT_DATE_FORMAT" }}, {{ item.received_by }}</dd>
+ {% endif %}
+ {% if item.reference %}
+ <dt>{% trans "Reference" %}</dt>
+ <dd>{{ item.reference }}</dd>
+ {% endif %}
+ <dt>{% trans "Confirmation" %}</dt>
+ <dd>{{ po.confirmation|default:"-" }}</dd>
+ {% endwith %}
+ <dt>{% trans "Purchase Price" %}</dt>
+ <dd>{{ form.instance.price|currency|default:"-" }}</dd>
+ </dl>
+ {% include "form_field_snippet.html" with field=form.amount %}
+ {% if item.product.is_serialized %}
+ {% include "form_field_snippet.html" with field=form.sn %}
+ {% endif %}
+ </form>
+{% endblock body %}
+
+{% block footer %}
+ <button class="btn" data-dismiss="modal">{% trans "Cancel" %}</button>
+{% if not item.received_at %}
+ <button type="submit" class="btn btn-primary">{% trans "Receive" %}</button>
+{% endif %}
+{% endblock footer %}
diff --git a/servo/templates/products/remove.html b/servo/templates/products/remove.html
new file mode 100755
index 0000000..e951656
--- /dev/null
+++ b/servo/templates/products/remove.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with code=product.code %}Really delete product "{{ code }}"?{% endblocktrans %}
+{% endblock header %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post">
+ {% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</buttom>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/products/search.html b/servo/templates/products/search.html
new file mode 100755
index 0000000..1e6485b
--- /dev/null
+++ b/servo/templates/products/search.html
@@ -0,0 +1,20 @@
+{% extends "two_column_layout.html" %}
+{% load i18n %}
+
+{% block first_column %}
+ <input type="text" class="input-large search-query filter" placeholder="{% trans "Filter results" %}"/>
+{% endblock first_column %}
+
+{% block second_column %}
+ {% if products %}
+ {% include "products/list.html" %}
+ {% include "pagination.html" with items=products %}
+ {% else %}
+ <h1 class="muted text-center">{% trans "No products found" %}</h1>
+ {% endif %}
+{% endblock second_column %}
+
+{% block crumbs %}
+ <li><a href="{% url 'products-list_products' %}">{% trans "Products" %}</a> <span class="divider">/</span></li>
+ <li class="active">{{ title }}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/products/tabs.html b/servo/templates/products/tabs.html
new file mode 100755
index 0000000..9bd871d
--- /dev/null
+++ b/servo/templates/products/tabs.html
@@ -0,0 +1,21 @@
+{% load i18n %}
+{% load servo_tags %}
+
+<div class="row-fluid">
+ <div class="span12">
+ <ul class="nav nav-tabs">
+ <li class="{% active request "products" %}">
+ <a href="{% url 'products-list_products' %}">{% trans "Products" %}</a>
+ </li>
+ <li class="{% active request "shipments" %}">
+ <a href="{% url 'shipments-list_incoming' %}">{% trans "Shipments" %}</a>
+ </li>
+ <li class="{% active request "purchases" %}">
+ <a href="{% url 'purchases-list_pos' %}">{% trans "Purchase Orders" %}</a>
+ </li>
+ <li class="{% active request "invoices" %}">
+ <a href="{% url 'invoices-index' %}">{% trans "Invoices" %}</a>
+ </li>
+ </ul>
+ </div>
+</div>
diff --git a/servo/templates/products/upload_gsx_parts.html b/servo/templates/products/upload_gsx_parts.html
new file mode 100755
index 0000000..1807343
--- /dev/null
+++ b/servo/templates/products/upload_gsx_parts.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Upload GSX Parts" %}
+{% endblock header %}
+
+{% block body %}
+ <form method="post" action="{{ action }}" enctype="multipart/form-data" class="form-horizontal" accept-charset="utf-8">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+ </form>
+{% endblock body %}
diff --git a/servo/templates/products/upload_products.html b/servo/templates/products/upload_products.html
new file mode 100755
index 0000000..746842b
--- /dev/null
+++ b/servo/templates/products/upload_products.html
@@ -0,0 +1,13 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block body %}
+<form method="post" action="{{ action }}" enctype="multipart/form-data" class="form-horizontal" accept-charset="utf-8">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+</form>
+{% endblock body %}
diff --git a/servo/templates/products/view.html b/servo/templates/products/view.html
new file mode 100755
index 0000000..7f5f355
--- /dev/null
+++ b/servo/templates/products/view.html
@@ -0,0 +1,205 @@
+{% extends "products/index.html" %}
+{% load servo_tags %}
+{% load mptt_tags %}
+{% load i18n %}
+
+{% block second_column %}
+{% block category_block %}
+<div class="span3">
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Categories" %}</li>
+ <li class="{% active request 'all' %}"><a href="{% url 'products-list_products' %}">{% trans "All" %}</a></li>
+ {% recursetree categories %}
+ <li class="{% active request 'products' node.slug '/' %}">
+ <a href="{% url 'products-list_products' group=node.slug %}">{{ node.title }}</a>
+ {% if not node.is_leaf_node %}
+ <ul class="nav nav-list">{{ children }}</ul>
+ {% endif %}
+ </li>
+ {% endrecursetree %}
+ </ul>
+</div>
+{% endblock category_block %}
+<div class="span3">
+ <ul class="nav nav-list">
+ <li class="nav-header">{% trans "Products" %}</li>
+ {% for i in products %}
+ <li class="{% active request group.slug i.pk %}">
+ <a href="{% url 'products-view_product' pk=i.pk group=group.slug %}">
+ <strong>{{ i.code }}</strong><span class="pull-right">{{ i.price_sales_stock|currency }}</span>
+ <br/>
+ <small>{{ i.title|truncatechars:30 }}</small>
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% include "pagination.html" with items=products %}
+</div>
+{% endblock second_column %}
+
+{% block third_column %}
+<div class="span6">
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Product" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Sales" %}</a></li>
+ <li><a href="#tab3" data-toggle="tab">{% trans "Purchases" %}</a></li>
+ <li><a href="#tab4" data-toggle="tab">{% trans "Invoices" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab1">
+ <div class="media">
+ <a class="pull-left" href="#">
+ <img class="media-object" src="{{ product.get_photo }}" data-src="{{ product.get_photo }}" width="64px"/>
+ </a>
+ <div class="media-body">
+ <h4 class="media-heading">{{ product.code }}</h4>
+ <p>{{ product.title }}</p>
+ <p>{{ product.description|default:"" }}</p>
+ </div>
+ </div>
+ <hr/>
+ <dl class="dl-horizontal">
+ <dt>{% trans "Stock Price" %}</dt>
+ <dd>{{ product.price_sales_stock|currency }}</dd>
+ <dt>{% trans "Exchange Price" %}</dt>
+ <dd>{{ product.price_sales_exchange|currency }}</dd>
+ {% if product.component_code %}
+ <dt>{% trans "Component Group" %}</dt>
+ <dd>{{ product.get_component_code_display }} ({{ product.get_part_type_display }})</dd>
+ {% endif %}
+ <dt>&nbsp;</dt>
+ <dd>
+ {% for t in product.tags.all %}
+ <span class="label">{{ t.tag }}</span>
+ {% endfor %}
+ </dd>
+ {% for i in inventory %}
+ {% ifchanged i.location.id %}
+ <hr/>
+ <h4>{{ i.location.title }}</h4>
+ {% endifchanged %}
+ <dl class="dl-horizontal">
+ <dt>{% trans "Stocked" %}</dt>
+ <dd>{{ i.amount_stocked }}</dd>
+ <dt>{% trans "Ordered" %}</dt>
+ <dd>{{ i.amount_ordered|default:"-" }}</dd>
+ <dt>{% trans "Reserved" %}</dt>
+ <dd>{{ i.amount_reserved|default:"-" }}</dd>
+ </dl>
+ {% endfor %}
+ <hr/>
+ {% with request.session.current_order_id as co %}
+ {% if co and product.pk %}
+ <a href="{% url 'orders-add_product' co product.pk %}" class="btn btn-primary"><i class="icon-share-alt icon-white"></i> {% trans "Use in" %} #{{ request.session.current_order_code }}</a>
+ {% endif %}
+ {% endwith %}
+ <a href="{% url 'orders-create_with_product' product.pk %}" class="btn">{% trans "Create Sales Order" %}</a>
+ <div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if perms.servo.change_product %}
+ {% if product.can_update_price %}
+ <li><a href="{% url 'products-update_price' product.pk %}">{% trans "Update Price" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Update Price" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a href="{% url 'products-delete_product' pk=product.pk group=group.slug %}" data-modal="#modal"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% else %}
+ <li><a href="#" class="disabled">{% trans "Update Price" %}</a></li>
+ <li class="divider"></li>
+ <li><a href="#" class="disabled"><i class="icon-trash"></i> {% trans "Delete" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ <div class="pull-right">
+ {% if perms.servo.change_product %}
+ <a href="{% url 'products-edit_product' pk=product.pk group=group.slug %}" class="btn">{% trans "Edit" %}</a>
+ {% else %}
+ <a href="#" class="btn disabled">{% trans "Edit" %}</a>
+ {% endif %}
+ </div>
+ </div>
+ <div class="tab-pane" id="tab2">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Order" %}</th>
+ <th>{% trans "Customer" %}</th>
+ <th>{% trans "Price" %}</th>
+ <th>{% trans "Created" %}</th>
+ <th>{% trans "Dispatched" %}</th>
+ </tr>
+ </thead>
+ {% for i in product.serviceorderitem_set.all reversed %}
+ <tr>
+ {% with i.order as order %}
+ <td><a href="{{ order.get_absolute_url }}">{{ order.code }}</a></td>
+ <td>{{ order.customer_name|default:"-" }}</td>
+ <td>{{ i.price|currency }}</td>
+ <td>{{ i.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ i.dispatched_at|date:"SHORT_DATE_FORMAT" }}</td>
+ {% endwith %}
+ </tr>
+ {% empty %}
+ <tr><td colspan="5" class="empty muted">{% trans "No Sales Orders" %}</td></tr>
+ {% endfor %}
+ </table>
+ </div>
+ <div class="tab-pane" id="tab3">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Order" %}</th>
+ <th>{% trans "Created" %}</th>
+ <th>{% trans "Price" %}</th>
+ <th>{% trans "Received" %}</th>
+ </tr>
+ </thead>
+ {% for i in product.purchaseorderitem_set.all reversed %}
+ <tr>
+ {% with i.purchase_order as order %}
+ <td><a href="{{ order.get_absolute_url }}">{{ order.pk }}</a></td>
+ <td>{{ i.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ i.price|currency }}</td>
+ <td>{{ i.received_at|date:"SHORT_DATE_FORMAT" }}</td>
+ {% endwith %}
+ </tr>
+ {% empty %}
+ <tr><td colspan="4" class="empty muted">{% trans "No Purchase Orders" %}</td></tr>
+ {% endfor %}
+ </table>
+ </div>
+ <div class="tab-pane" id="tab4">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Invoice" %}</th>
+ <th>{% trans "Order" %}</th>
+ <th>{% trans "Created" %}</th>
+ <th>{% trans "Price" %}</th>
+ </tr>
+ </thead>
+ {% for i in product.invoiceitem_set.all reversed %}
+ <tr>
+ <td><a href="{{ i.invoice.get_absolute_url }}">{{ i.invoice.pk }}</a></td>
+ {% with i.invoice.order as order %}
+ <td><a href="{{ order.get_absolute_url }}">{{ order.code }}</a></td>
+ {% endwith %}
+ <td>{{ i.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ i.price|currency }}</td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="4" class="empty muted">{% trans "No invoices" %}</td></tr>
+ {% endfor %}
+ </table>
+ </div>
+ </div>
+</div>
+{% endblock third_column %}
+
+{% block breadcrumb %}
+<li class="active"><span class="divider">/</span>{{ product.title }}</li>
+{% endblock breadcrumb %}
diff --git a/servo/templates/products/view_incoming.html b/servo/templates/products/view_incoming.html
new file mode 100755
index 0000000..8404ab3
--- /dev/null
+++ b/servo/templates/products/view_incoming.html
@@ -0,0 +1 @@
+{% extends "shipments/list_incoming.html" %}
diff --git a/servo/templates/purchases/edit_po.html b/servo/templates/purchases/edit_po.html
new file mode 100755
index 0000000..618f1a3
--- /dev/null
+++ b/servo/templates/purchases/edit_po.html
@@ -0,0 +1,92 @@
+{% extends "purchases/list_pos.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a class="btn" href="{% url 'purchases-choose_for_po' order.pk %}" data-modal="#modal"><i class="icon-shopping-cart"></i> {% trans "Add Product" %}</a>
+{% endblock toolbar %}
+
+{% block filter_bar %}
+{% endblock filter_bar %}
+
+{% block detail_view %}
+ <form method="post" accept-charset="utf-8" action="">
+ <div class="span3">
+ {% include "form_field_snippet.html" with field=form.sales_order %}
+ {% include "form_field_snippet.html" with field=form.reference %}
+ {% include "form_field_snippet.html" with field=form.confirmation %}
+ </div>
+ <div class="span9">
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab1" data-toggle="tab">{% trans "Products" %}</a></li>
+ <li><a href="#tab2" data-toggle="tab">{% trans "Dispatch" %}</a></li>
+ </ul>
+ <div class="tab-content">
+ {% csrf_token %}
+ {{ formset.management_form }}
+ <div class="tab-pane active" id="tab1">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Code" %}</th>
+ <th>{% trans "Product" %}</th>
+ <th>{% trans "Reference" %}</th>
+ <th>{% trans "Amount" %}</th>
+ <th>{% trans "Purchase Price" %}</th>
+ {% if not order.submitted_at %}
+ <th>{% trans "Delete" %}</th>
+ {% else %}
+ <th></th>
+ {% endif %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for form in formset %}
+ {{ form.id }}
+ {{ form.product }}
+ <tr>
+ <td>{{ form.code }}</td>
+ <td>{{ form.title }}</td>
+ <td>{{ form.reference }}</td>
+ <td>{{ form.amount }}</td>
+ <td>{{ form.price }}</td>
+ {% if not order.submitted_at %}
+ <td>{{ form.DELETE }}</td>
+ {% else %}
+ <td></td>
+ {% endif %}
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="6" class="muted empty">{% trans "Order does not contain any products" %}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td colspan="3" style="text-align:right">{% trans "Total" %}:</td>
+ <td>{{ order.amount }}</td>
+ <td>{{ order.sum|currency }}</td>
+ <td></td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ <div class="tab-pane" id="tab2">
+ {% include "form_field_snippet.html" with field=form.carrier %}
+ {% include "form_field_snippet.html" with field=form.supplier %}
+ {% include "form_field_snippet.html" with field=form.tracking_id %}
+ {% include "form_field_snippet.html" with field=form.days_delivered %}
+ </div>
+ </div>
+ {% if not order.submitted_at %}
+ <div class="form-actions">
+ <label class="checkbox pull-left">
+ <input type="checkbox" name="confirm" id="id_confirm"> {% trans "Confirm" %}
+ </label>
+ <div class="pull-right"><button type="submit" class="btn btn-primary" id="save-bulk-return" data-placeholder="{% trans "Submit" %}">{% trans "Save" %}</button></div>
+ </div>
+ {% endif %}
+ </div>
+ </form>
+{% endblock detail_view %}
diff --git a/servo/templates/purchases/list_pos.html b/servo/templates/purchases/list_pos.html
new file mode 100755
index 0000000..f580598
--- /dev/null
+++ b/servo/templates/purchases/list_pos.html
@@ -0,0 +1,132 @@
+{% extends "default.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+<div class="btn-group">
+ <a href="{% url 'purchases-create_po' %}" class="btn"><i class="icon-plus"></i> {% trans "New" %}</a>
+</div>
+{% endblock toolbar %}
+
+{% block content %}
+
+<div class="row-fluid row-header">
+ <div class="span12">
+ <form class="form-search pull-right">
+
+ </form>
+ </div>
+</div>
+
+{% include "products/tabs.html" %}
+
+{% block filter_bar %}
+<div class="row-fluid">
+ <div class="span12 well">
+ <form action="" method="post" class="form-inline">
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <div class="control-group">
+ <label class="control-label">&nbsp;</label>
+ <div class="controls">
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+</div>
+{% endblock filter_bar %}
+
+<div class="row-fluid">
+ <div class="span12">
+ {% block detail_view %}
+ <table class="table table-hover">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>{% trans "Created" %}</th>
+ <th>{% trans "Ordered" %}</th>
+ <th>{% trans "Reference" %}</th>
+ <th>{% trans "Confirmation" %}</th>
+ <th>{% trans "Received" %}</th>
+ <th>{% trans "Order" %}</th>
+ <th>{% trans "Products" %}</th>
+ <th>{% trans "Total" %}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for po in orders %}
+ <tr>
+ <td><a href="{{ po.get_absolute_url }}">{{ po.id|safe }}</a></td>
+ <td>{{ po.created_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ po.submitted_at|date:"SHORT_DATE_FORMAT" }}</td>
+ <td>{{ po.reference|default:"" }}</td>
+ <td>{{ po.confirmation|default:"" }}</td>
+ <td>{{ po.date_arrived }}</td>
+ {% if po.sales_order %}
+ <td><a href="{% url 'orders-edit' po.sales_order.pk %}">{{ po.sales_order.code }}</a></td>
+ {% else %}
+ <td></td>
+ {% endif %}
+ <td>{{ po.amount }}</td>
+ <td>{{ po.sum|currency }}</td>
+ <td>
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ {% if po.can_create_gsx_stock %}
+ <li><a href="{% url 'purchases-submit_stock_order' po.id %}" data-modal="#modal">{% trans "Create GSX Stocking Order" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Create GSX Stocking Order" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ {% if perms.servo.change_purchaseorder and po.is_editable %}
+ <li><a href="{{ po.get_absolute_url }}">{% trans "Edit" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Edit" %}</a></li>
+ {% endif %}
+ {% if perms.servo.delete_purchaseorder and po.is_editable %}
+ <li><a href="{% url 'purchases-delete_po' po.id %}">{% trans "Delete" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Delete" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ </td>
+ </tr>
+ {% empty %}
+ <tr><td colspan="10" class="empty muted">{% trans "No Purchase Orders" %}</td></tr>
+ {% endfor %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td><strong>{{ total.total__sum|currency|default:"" }}</strong></td>
+ <td></td>
+ </tr>
+ </tfoot>
+ </table>
+ {% include "pagination.html" with items=orders %}
+ {% endblock detail_view %}
+ </div>
+</div>
+{% endblock content %}
+
+{% block crumbs %}
+<li><a href="{% url 'purchases-list_pos' %}">{% trans "Purchase Orders" %}</a></li>
+{% endblock crumbs %}
diff --git a/servo/templates/purchases/order_stock.html b/servo/templates/purchases/order_stock.html
new file mode 100755
index 0000000..fe3f4a0
--- /dev/null
+++ b/servo/templates/purchases/order_stock.html
@@ -0,0 +1,17 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "Submit this stocking order?" %}
+{% endblock header %}
+
+{% block body %}
+ {% trans "Products will be ordered from GSX." %}
+{% endblock body %}
+
+{% block footer %}
+ <form action="{{ action }}" method="post">
+ {% csrf_token %}
+ <button class="btn btn-danger">{% trans "Submit" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/purchases/view_po.html b/servo/templates/purchases/view_po.html
new file mode 100755
index 0000000..c43a058
--- /dev/null
+++ b/servo/templates/purchases/view_po.html
@@ -0,0 +1,56 @@
+{% extends "purchases/edit_po.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+{% endblock toolbar %}
+
+{% block filter_bar %}
+{% endblock filter_bar %}
+
+{% block detail_view %}
+ <div class="row">
+ <div class="span4">
+ <dl class="dl-horizontal">
+ <dt>{% trans "Created By" %}</dt>
+ <dd>{{ po.created_by }}</dd>
+ <dt>{% trans "Created At" %}</dt>
+ <dd>{{ po.created_at|date:"SHORT_DATE_FORMAT" }}</dd>
+ <dt>{% trans "Sales Order" %}</dt>
+ <dd>{{ po.sales_order|default:"" }}</dd>
+ <dt>{% trans "Reference" %}</dt>
+ <dd>{{ po.reference }}</dd>
+ <dt>{% trans "Confirmation" %}</dt>
+ <dd>{{ po.confirmation }}</dd>
+ </dl>
+ </div>
+ <div class="span8">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>{% trans "Product" %}</th>
+ <th>{% trans "Reference" %}</th>
+ <th>{% trans "Amount" %}</th>
+ <th>{% trans "Purchase Price" %}</th>
+ <th>{% trans "Received At" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for i in po.purchaseorderitem_set.all %}
+ <tr>
+ <td><strong>{{ i.code }}</strong><br/>{{ i.title }}</td>
+ <td>{{ i.reference }}</td>
+ <td>{{ i.amount }}</td>
+ <td>{{ i.price|currency }}</td>
+ <td>{{ i.received_at|date:"SHORT_DATE_FORMAT"|default:"-" }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+{% endblock detail_view %}
+
+{% block crumbs %}
+ <li><a href="{% url 'purchases-list_pos' %}">{% trans "Purchase Orders" %}</a></li>
+{% endblock crumbs %}
diff --git a/servo/templates/repairs/add_part.html b/servo/templates/repairs/add_part.html
new file mode 100755
index 0000000..2236631
--- /dev/null
+++ b/servo/templates/repairs/add_part.html
@@ -0,0 +1,22 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% blocktrans with repair=repair.confirmation %}Add part to repair {{ repair }}?{% endblocktrans %}
+{% endblock header %}
+
+{% block body %}
+<form method="post" action="{{ action }}">
+ <div class="media">
+ <a class="pull-left" href="#">
+ <img class="media-object" src="{{ item.product.get_photo }}" style="width:120px"/>
+ </a>
+ <div class="media-body">
+ <h4 class="media-heading">{{ item.code }}</h4>
+ <p>{{ item.title }}</p>
+ <p>{{ item.get_comptia_code_display }}, {{ item.get_price_category_display }}</p>
+ </div>
+ </div>
+ {% csrf_token %}
+</form>
+{% endblock body %}
diff --git a/servo/templates/repairs/check_parts.html b/servo/templates/repairs/check_parts.html
new file mode 100755
index 0000000..519044e
--- /dev/null
+++ b/servo/templates/repairs/check_parts.html
@@ -0,0 +1,10 @@
+{% load servo_tags %}
+{% for p in parts %}
+ <label class="checkbox">
+ <input type="checkbox" {% if p.pk in checked_parts %}checked="checked"{% endif %} class="toggle_part" name="parts" value="{{ p.pk|safe }}"/><small><strong>{{ p.code }}</strong></small>
+ <small class="pull-right">{{ p.price|currency }}</small>
+ </label>
+ <small class="muted">{{ p.title }}</small><br/>
+ <small class="muted">{{ p.get_price_category_display }} - {{ p.warranty_status }}</small>
+ <hr class="dashed"/>
+{% endfor %}
diff --git a/servo/templates/repairs/delete_part.html b/servo/templates/repairs/delete_part.html
new file mode 100755
index 0000000..4a80824
--- /dev/null
+++ b/servo/templates/repairs/delete_part.html
@@ -0,0 +1,12 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Remove this part from the GSX repair?" %}
+{% endblock header %}
+
+{% block footer %}
+<form method="post" action="{{ action }}">{% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+</form>
+{% endblock footer %}
diff --git a/servo/templates/repairs/delete_repair.html b/servo/templates/repairs/delete_repair.html
new file mode 100755
index 0000000..3f68478
--- /dev/null
+++ b/servo/templates/repairs/delete_repair.html
@@ -0,0 +1,16 @@
+{% extends "snippets/modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Delete this GSX repair?" %}
+{% endblock header %}
+
+{% block body %}
+{% trans "Only repairs that have not been submitted can be deleted." %}
+{% endblock body %}
+
+{% block footer %}
+<form method="post" action="{{ action }}">{% csrf_token %}
+ <button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
+</form>
+{% endblock footer %}
diff --git a/servo/templates/repairs/get_details.html b/servo/templates/repairs/get_details.html
new file mode 100755
index 0000000..a6fe914
--- /dev/null
+++ b/servo/templates/repairs/get_details.html
@@ -0,0 +1,27 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ repair.dispatch_id }}
+{% endblock header %}
+
+{% block body %}
+<dl class="dl-horizontal">
+ <dt>{% trans "Service Order" %}</dt>
+ <dd>{{ repair.po_number|safe }}</dd>
+ <dt>{% trans "Warranty Coverage" %}</dt>
+ <dd>{{ repair.status_description|default:"-" }}</dd>
+ <dt>{% trans "CS Code" %}</dt>
+ <dd>{{ repair.cs_code|default:"-" }}</dd>
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ repair.notes|default:"-" }}</dd>
+ <dt>{% trans "Tracking Number" %}</dt>
+ <dd>{{ repair.tracking_number|default:"-" }}</dd>
+ <dt>{% trans "Parts" %}</dt>
+ <dd>
+ {% for p in repair.parts %}
+ <strong>{{ p.partNumber }}</strong> {{ p.partDescription }}<br/>
+ {% endfor %}
+ </dd>
+</dl>
+{% endblock body %}
diff --git a/servo/templates/repairs/part_menu.html b/servo/templates/repairs/part_menu.html
new file mode 100755
index 0000000..c314496
--- /dev/null
+++ b/servo/templates/repairs/part_menu.html
@@ -0,0 +1,33 @@
+{% load i18n %}
+<ul class="dropdown-menu">
+{% if part.can_return %}
+ <li><a href="{% url 'parts-return_label' repair.pk part.pk %}" class="window">{% trans "Print Return Label" %}</a></li>
+ <li><a href="{% url 'parts-register_return' part.pk %}">{{ part.get_return_title }}</a></li>
+{% else %}
+ <li class="disabled"><a href="#" title="{% trans 'Part has no return order number' %}">{% trans "Print Return Label" %}</a></li>
+ <li class="disabled"><a href="#">{% trans "Register for Return" %}</a></li>
+{% endif %}
+{% if part.can_update_sn %}
+ <li><a href="{% url 'repairs-update_sn' repair.pk part.pk %}">{% trans "Update Serial Numbers" %}</a></li>
+{% else %}
+ <li class="disabled"><a href="#">{% trans "Update Serial Numbers" %}</a></li>
+{% endif %}
+ <li class="divider"></li>
+{% if repair.is_open %}
+ <li><a href="{% url 'shipments-update_part' part=part.pk return_type=1 %}" data-modal="#modal" class="spin">{% trans "Return DOA" %}</a></li>
+ <li><a href="{% url 'shipments-update_part' part=part.pk return_type=2 %}" data-modal="#modal" class="spin">{% trans "Return Good Part" %}</a></li>
+ <li><a href="{% url 'shipments-update_part' part=part.pk return_type=3 %}" data-modal="#modal" class="spin">{% trans "Convert to Stock" %}</a></li>
+ <li class="divider"></li>
+{% if part.line_number %}
+ <li><a href="{% url 'repairs-remove_part' repair.pk part.pk %}" data-modal="#modal">{% trans "Remove from Repair" %}</a></li>
+{% else %}
+ <li class="disabled"><a href="#">{% trans "Remove from Repair" %}</a></li>
+{% endif %}
+{% else %}
+ <li class="disabled"><a href="#">{% trans "Return DOA" %}</a></li>
+ <li class="disabled"><a href="#">{% trans "Return Good Part" %}</a></li>
+ <li class="disabled"><a href="#">{% trans "Convert to Stock" %}</a></li>
+ <li class="divider"></li>
+ <li class="disabled"><a href="#">{% trans "Remove from Repair" %}</a></li>
+{% endif %}
+</ul>
diff --git a/servo/templates/rules/form.html b/servo/templates/rules/form.html
new file mode 100644
index 0000000..ac7a35c
--- /dev/null
+++ b/servo/templates/rules/form.html
@@ -0,0 +1,142 @@
+{% extends "rules/list_rules.html" %}
+{% load bootstrap3 %}
+{% load static %}
+{% load i18n %}
+
+{% block third_column %}
+<div style="margin-top:28px">
+ <form method="post" action="" accept-charset="utf-8" class="form-horizontal" data-bind="submit: validateAndSave">
+ {% csrf_token %}
+ <div class="form-group">
+ <input class="form-control" name="description" type="text" autocomplete="off" data-bind="value: rule.description"/>
+ </div>
+ <fieldset>
+ <legend>If <select name="match" data-bind="options: rule.matchChoices, optionsText: 'title', optionsValue: 'key', value: rule.match"></select> of the conditions are met:</legend>
+ <table>
+ <tbody data-bind="foreach: rule.conditions">
+ <tr>
+ <td>
+ <div class="form-group">
+ <select class="form-control" data-bind="options: keyChoices, optionsText: 'title', optionsValue: 'key', value: key" name="condition-key">
+ </select>
+ </div>
+ </td>
+ <td>
+ <div class="form-group">
+ <!-- ko if: canSelect() -->
+ <select class="form-control" name="condition-value" data-bind="options: valueChoices, optionsText: 'title', optionsValue: 'value'">
+ </select>
+ <!-- /ko -->
+ <!-- ko if: !canSelect() -->
+ <input class="form-control" type="text" name="condition-value" required="required" data-bind="value: value"/>
+ <!-- /ko -->
+ </div>
+ </td>
+ <td>
+ <a href="#" class="btn btn-default" data-bind="click: $parent.removeCondition, css: { disabled: $index() == 0 }"><i class="icon-minus"></i></a> <a href="#" class="btn btn-default" data-bind="click: $parent.addCondition"><i class="icon-plus"></i></a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ <fieldset>
+ <legend>{% trans "Peform the following actions" %}:</legend>
+ <table>
+ <tbody data-bind="foreach: rule.actions">
+ <tr>
+ <td>
+ <div class="form-group" id="key">
+ <select class="form-control" data-bind="options: keyChoices, optionsText: 'title', optionsValue: 'key', value: key" name="action-key"></select>
+ </div>
+ </td>
+ <td>
+ <!-- ko if: canSelect() -->
+ <select class="form-control" name="action-value" data-bind="options: valueChoices, optionsText: 'title', optionsValue: 'value'">
+ </select>
+ <!-- /ko -->
+ <!-- ko if: !canSelect() -->
+ <input type="text" name="action-value" data-bind="value: value"/>
+ <!-- /ko -->
+ </td>
+ <td>
+ <a class="btn btn-default" data-bind="click: $parent.removeAction, css: { disabled: $index() == 0 }" href="#"><i class="icon-minus"></i></a> <a class="btn btn-default" data-bind="click: $parent.addAction" href="#"><i class="icon-plus"></i></a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ <hr/>
+ {% buttons %}
+ <div class="form-controls pull-right">
+ {% if rule.pk %}
+ <a class="btn btn-danger" href="{% url 'rules-delete_rule' rule.pk %}" data-modal="#modal">{% trans "Delete" %}</a>
+ {% else %}
+ <a class="btn btn-danger disabled" href="#">{% trans "Delete" %}</a>
+ {% endif %}
+ <a class="btn btn-default" href="{% url 'rules-list_rules' %}">{% trans "Cancel" %}</a>
+ <button type="submit" class="btn btn-primary" data-bind="disable: rule.description().length < 3">{% trans "OK" %}</button>
+ </div>
+ </form>
+ </div>
+ {% endbuttons %}
+{% endblock third_column %}
+
+{% block media %}
+ <script type="text/javascript" src="{% static "js/knockout.js" %}"></script>
+ <script type="text/javascript" src="{% static "js/rules.js" %}"></script>
+ <script type="text/javascript">
+ // start data init
+ var cData = [];
+ cData['QUEUE'] = [];
+ cData['STATUS'] = [];
+
+ var aData = [];
+ aData['ADD_TAG'] = [];
+ aData['SET_USER'] = [];
+ aData['SET_QUEUE'] = [];
+ aData['SEND_EMAIL'] = [];
+
+ // get the possible choices
+ $.get('/api/statuses/', function(r){
+ r.forEach(function(e){
+ var choice = {title: e.fields.title, value: e.pk};
+ cData['STATUS'].push(choice);
+ });
+ });
+
+ $.get('/api/queues/', function(r){
+ r.forEach(function(e){
+ var choice = {title: e.fields.title, value: e.pk};
+ cData['QUEUE'].push(choice);
+ aData['SET_QUEUE'].push(choice);
+ });
+
+ var viewModel = new ViewModel(cData, aData);
+ ko.applyBindings(viewModel);
+
+ {% if rule.pk %}
+ var data = {{ rule.serialize|safe }};
+ viewModel.rule.description(data.description);
+ viewModel.rule.match(data.match);
+ var conditions = [];
+ data.conditions.forEach(function(e){
+ var c = new Condition(cData);
+ c.key(e.key);
+ c.value(e.value);
+ c.operator(e.operator);
+ conditions.push(c);
+ });
+ viewModel.rule.conditions(conditions);
+ var actions = [];
+ data.actions.forEach(function(e){
+ var c = new Action(aData);
+ c.key(e.key);
+ c.value(e.value);
+ actions.push(c);
+ });
+ viewModel.rule.actions(actions);
+ {% endif %}
+ });
+ // end data init
+ </script>
+{% endblock media %}
diff --git a/servo/templates/rules/list.html b/servo/templates/rules/list.html
new file mode 100644
index 0000000..3e9ea9e
--- /dev/null
+++ b/servo/templates/rules/list.html
@@ -0,0 +1,6 @@
+{% extends "admin/index.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a class="btn" href="{% url 'rules-create' %}"><i class="icon-plus"></i> {% trans "New Rule" %}</a>
+{% endblock toolbar %}
diff --git a/servo/templates/rules/list_rules.html b/servo/templates/rules/list_rules.html
new file mode 100644
index 0000000..9aef78e
--- /dev/null
+++ b/servo/templates/rules/list_rules.html
@@ -0,0 +1,22 @@
+{% extends "admin/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a class="btn" href="{% url 'rules-create' %}"><i class="icon-plus"></i> {% trans "New Rule" %}</a>
+{% endblock toolbar %}
+
+{% block second_column %}
+ <div class="span3">
+ <ul class="nav nav-pills nav-stacked">
+ <li class="nav-header">{% trans "Rules" %}</li>
+ {% for o in object_list %}
+ <li class="{% active_url request o.get_admin_url %}"><a href="{{ o.get_admin_url }}">{{ o.get_name }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="span9">
+ {% block third_column %}
+ {% endblock third_column %}
+ </div>
+{% endblock second_column %}
diff --git a/servo/templates/search/results/articles.html b/servo/templates/search/results/articles.html
new file mode 100755
index 0000000..8ea7535
--- /dev/null
+++ b/servo/templates/search/results/articles.html
@@ -0,0 +1,10 @@
+{% extends "search/spotlight.html" %}
+{% load humanize %}
+{% load servo_tags %}
+
+{% block second_column %}
+{% for i in articles %}
+ <h2>{{ i.title }}</h2>
+ {{ i.content|markdown }}
+{% endfor %}
+{% endblock second_column %}
diff --git a/servo/templates/search/results/customers.html b/servo/templates/search/results/customers.html
new file mode 100755
index 0000000..6203598
--- /dev/null
+++ b/servo/templates/search/results/customers.html
@@ -0,0 +1,26 @@
+{% extends "search/spotlight.html" %}
+{% load i18n %}
+
+{% block second_column %}
+ {% if customers %}
+ {% for i in customers %}
+ <address>
+ <strong><a href="{% url 'customers-view_customer' i.pk %}">{{ i.name }}</a></strong><br>
+ {% if i.street_address %}
+ {{ i.street_address }}<br/>
+ {{ i.zip_code }}, {{ i.city }}<br/>
+ {% endif %}
+ {% if i.phone %}
+ <abbr title="{% trans "Phone" %}">P:</abbr> {{ i.phone }}
+ {% endif %}
+ {% if i.email %}
+ <br/>
+ <a href="{% url 'notes-create_to_customer' customer=i.pk %}"><i class="icon-envelope"></i> {{ i.email }}</a>
+ {% endif %}
+ </address>
+ <hr/>
+ {% endfor %}
+ {% else %}
+ <h1 class="muted text-center">{% trans "No customers found" %}</h1>
+ {% endif %}
+{% endblock second_column %}
diff --git a/servo/templates/search/results/devices.html b/servo/templates/search/results/devices.html
new file mode 100755
index 0000000..d133d55
--- /dev/null
+++ b/servo/templates/search/results/devices.html
@@ -0,0 +1,10 @@
+{% extends "search/spotlight.html" %}
+{% load i18n %}
+
+{% block second_column %}
+ {% if devices %}
+ {% include "devices/list.html" %}
+ {% else %}
+ <h1 class="muted text-center">{% trans "No devices found" %}</h1>
+ {% endif %}
+{% endblock second_column %}
diff --git a/servo/templates/search/results/gsx.html b/servo/templates/search/results/gsx.html
new file mode 100755
index 0000000..ac8471b
--- /dev/null
+++ b/servo/templates/search/results/gsx.html
@@ -0,0 +1,41 @@
+{% extends "search/spotlight.html" %}
+{% load i18n %}
+
+{% block second_column %}
+{% block tabs %}
+ <ul class="nav nav-tabs" id="gsx-tabs">
+ {% if gsx_type == 'serialNumber' or 'alternateDeviceId' %}
+ <li><a href="{% url 'search-gsx' what='warranty' %}?q={{ query }}">{% trans "Device" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Device" %}</a></li>
+ {% endif %}
+ {% if gsx_type == 'serialNumber' or gsx_type == 'partNumber' %}
+ <li><a href="{% url 'search-gsx' what='parts' %}?q={{ query }}">{% trans "Parts" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Parts" %}</a></li>
+ {% endif %}
+ {% if gsx_type == 'serialNumber' or gsx_type == 'dispatchId' %}
+ <li><a href="{% url 'search-gsx' what='repairs' %}?q={{ query }}">{% trans "Repairs" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="#">{% trans "Repairs" %}</a></li>
+ {% endif %}
+ <li class="pull-right"><a href="#" class="filter_table"><i class="icon-search"></i></a></li>
+ </ul>
+{% endblock tabs %}
+
+{% block results %}
+ <div id="gsx-container" data-source="{% url 'search-gsx_results' what=what %}?q={{ query }}">
+ <div class="progress active">
+ <div class="bar" style="width:1%;" data-progress="0"></div>
+ </div>
+ </div>
+{% endblock results %}
+
+{% endblock second_column %}
+
+{% block media %}
+<script type="text/javascript">
+ var loc = location.pathname + location.search;
+ $('#gsx-tabs>li>a[href="'+loc+'"]').parent().addClass('active');
+</script>
+{% endblock media %}
diff --git a/servo/templates/search/results/gsx_error.html b/servo/templates/search/results/gsx_error.html
new file mode 100755
index 0000000..0c72d03
--- /dev/null
+++ b/servo/templates/search/results/gsx_error.html
@@ -0,0 +1,4 @@
+{% load i18n %}
+<table class="table">
+ <tr><td class="muted empty">{{ message }}</td></tr>
+</table>
diff --git a/servo/templates/search/results/gsx_notfound.html b/servo/templates/search/results/gsx_notfound.html
new file mode 100755
index 0000000..87261bf
--- /dev/null
+++ b/servo/templates/search/results/gsx_notfound.html
@@ -0,0 +1,4 @@
+{% load i18n %}
+<table class="table">
+ <tr><td class="muted empty">{% trans "No search results" %}</td></tr>
+</table>
diff --git a/servo/templates/search/results/gsx_repair_details.html b/servo/templates/search/results/gsx_repair_details.html
new file mode 100755
index 0000000..81908b5
--- /dev/null
+++ b/servo/templates/search/results/gsx_repair_details.html
@@ -0,0 +1,19 @@
+{% load i18n %}
+
+{% for r in results %}
+<h3>{{ r.dispatchId }}</h3>
+<dl class="dl-horizontal">
+ <dt>{% trans "Service Order" %}</dt>
+ <dd>{{ r.purchaseOrderNumber }}</dd>
+ <dt>{% trans "CS Code" %}</dt>
+ <dd>{{ r.csCode|safe }}</dd>
+ <dt>{% trans "Tracking Number" %}</dt>
+ <dd>{{ r.deliveryTrackingNumber }}</dd>
+ <dt>{% trans "Notes" %}</dt>
+ <dd>{{ r.notes }}</dd>
+ <dt>{% trans "Warranty Coverage" %}</dt>
+ <dd>{{ r.coverageStatusDescription }}</dd>
+ <dt>{% trans "Status" %}</dt>
+ <dd>{{ r.orderStatus }}</dd>
+</dl>
+{% endfor %}
diff --git a/servo/templates/search/results/gsx_results.html b/servo/templates/search/results/gsx_results.html
new file mode 100755
index 0000000..dc7fff5
--- /dev/null
+++ b/servo/templates/search/results/gsx_results.html
@@ -0,0 +1,5 @@
+<div id="gsx-container" data-source="{% url 'search-search_gsx' what=what arg=arg value=value %}">
+ <div class="progress active">
+ <div class="bar" style="width:1%;" data-progress="0"></div>
+ </div>
+</div>
diff --git a/servo/templates/search/results/gsx_warranty.html b/servo/templates/search/results/gsx_warranty.html
new file mode 100755
index 0000000..4682885
--- /dev/null
+++ b/servo/templates/search/results/gsx_warranty.html
@@ -0,0 +1,49 @@
+{% load i18n %}
+
+{% for d in results %}
+<div class="row-fluid">
+ <div class="span3">
+ <img class="img-rounded" src="{{ d.image_url }}" alt="{{ d.description }}" title="{{ d.description }}"/>
+ </div>
+ <div class="span9" id="gsx-results">
+ <h3>{{ d.description }}</h3>
+ <dl class="dl-horizontal">
+ <dt>{% trans "Warranty Status" %}</dt>
+ <dd>{{ d.get_warranty_status_display }}</dd>
+ <dt>{% trans "Purchase Date" %}</dt>
+ <dd>{{ d.purchased_on|date:"SHORT_DATE_FORMAT"|default:"-" }}, {{ d.purchase_country|default:"-" }}</dd>
+ <dt>{% trans "Serial Number" %}</dt>
+ <dd>{{ d.sn }}</dd>
+ <dt>{% trans "Configration" %}</dt>
+ <dd>{{ d.configuration }}</dd>
+ {% if d.activation %}
+ <dt>IMEI</dt>
+ <dd>{{ d.activation.imeiNumber }}</dd>
+ <dt>{% trans "Activation Profile" %}</dt>
+ <dd>{{ d.activation.initialActivationPolicyDetails }}</dd>
+ <dt>{% trans "Unlocked" %}</dt>
+ <dd>{{ d.activation.unlocked }}</dd>
+ <dt>{% trans "Find My iPhone" %}</dt>
+ <dd>{{ d.fmip_is_active|yesno:"Active,Inactive" }}</dd>
+ {% endif %}
+ </dl>
+ <div class="btn-group pull-right">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <i class="icon-cog"></i> <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="{% url 'orders-create_with_sn' sn=query %}">{% trans "Create Service Order" %}</a></li>
+ {% if request.session.current_order_id %}
+ <li><a href="{% url 'orders-add_device' pk=request.session.current_order_id sn=query %}">{% trans "Use in order" %} #{{ request.session.current_order.code }}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ {% if d.manualURL %}
+ <li><a href="{{ d.manualURL }}">{% trans "Download Manual" %}</a></li>
+ {% else %}
+ <li class="disabled"><a href="">{% trans "Download Manual" %}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+ </div>
+</div>
+{% endfor %}
diff --git a/servo/templates/search/results/notes.html b/servo/templates/search/results/notes.html
new file mode 100755
index 0000000..ec2547a
--- /dev/null
+++ b/servo/templates/search/results/notes.html
@@ -0,0 +1,20 @@
+{% extends "search/spotlight.html" %}
+{% load servo_tags %}
+{% load humanize %}
+
+{% block second_column %}
+<ul class="media-list">
+ {% for note in notes %}
+ <li class="media">
+ <a class="pull-left" href="#">
+ <img src="{{ note.created_by.get_avatar }}" alt="{{ note.created_by }}" title="{{ note.created_by }}" class="img-rounded avatar"/>
+ </a>
+ <div class="media-body">
+ <h5 class="media-heading">{{ note.get_sender_name }} {{ note.created_at|naturaltime }}{% if note.order %} <a href="{% url 'orders-edit' note.order.pk %}#note-{{ note.pk }}"><i class="icon-share-alt"></i></a>{% endif %}</h5>
+ {{ note.body|markdown }}
+ </div>
+ <hr/>
+ </li>
+ {% endfor %}
+</ul>
+{% endblock second_column %}
diff --git a/servo/templates/search/results/orders.html b/servo/templates/search/results/orders.html
new file mode 100755
index 0000000..1e1fc6f
--- /dev/null
+++ b/servo/templates/search/results/orders.html
@@ -0,0 +1,10 @@
+{% extends "search/spotlight.html" %}
+{% load i18n %}
+
+{% block second_column %}
+ {% if orders %}
+ {% include "orders/list.html" %}
+ {% else %}
+ <h1 class="muted text-center">{% trans "No orders found" %}</h1>
+ {% endif %}
+{% endblock second_column %}
diff --git a/servo/templates/search/results/products.html b/servo/templates/search/results/products.html
new file mode 100755
index 0000000..32afb5c
--- /dev/null
+++ b/servo/templates/search/results/products.html
@@ -0,0 +1,10 @@
+{% extends "search/spotlight.html" %}
+{% load i18n %}
+
+{% block second_column %}
+ {% if products %}
+ {% include "products/list.html" %}
+ {% else %}
+ <h1 class="muted text-center">{% trans "No products found" %}</h1>
+ {% endif %}
+{% endblock second_column %}
diff --git a/servo/templates/search/spotlight.html b/servo/templates/search/spotlight.html
new file mode 100755
index 0000000..e372b23
--- /dev/null
+++ b/servo/templates/search/spotlight.html
@@ -0,0 +1,37 @@
+{% extends "two_column_layout.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'customers-create_customer' group='all' %}?name={{ query }}" class="btn btn-inverse"><i class="icon-plus icon-white"></i> {% trans "New Customer" %}</a>
+{% endblock toolbar %}
+
+{% block first_column %}
+<ul class="nav nav-list">
+ <li class="nav-header">{% trans "Results" %}</li>
+ <li class="{% active request 'search/customers' %}">
+ <a href="/search/customers/?q={{ query }}">{% trans "Customers" %}</a>
+ </li>
+ <li class="{% active request 'search/devices' %}">
+ <a href="/search/devices/?q={{ query }}">{% trans "Devices" %}</a>
+ </li>
+ <li class="{% active request 'search/gsx' %}">
+ <a href="/search/gsx/{{ what }}?q={{ query }}">{% trans "GSX" %}</a>
+ </li>
+ <li class="{% active request 'search/orders' %}">
+ <a href="/search/orders/?q={{ query }}">{% trans "Orders" %}</a>
+ </li>
+ <li class="{% active request 'search/products' %}">
+ <a href="/search/products/?q={{ query }}">{% trans "Products and Parts" %}</a>
+ </li>
+ <li class="{% active request 'search/notes' %}"><a href="/search/notes/?q={{ query }}">{% trans "Notes" %}</a></li>
+ <li class="{% active request 'search/articles' %}"><a href="/search/articles/?q={{ query }}">{% trans "Articles" %}</a></li>
+</ul>
+{% endblock first_column %}
+
+{% block second_column %}
+{% endblock second_column %}
+
+{% block footer %}
+ <li><i class="icon-home"></i> <a href="{% url 'accounts-list_orders' request.user.username %}">{% trans "Home" %}</a> <span class="divider">/</span></li><li class="active">{{ title }}</li>
+{% endblock footer %}
diff --git a/servo/templates/shipments/add_to_return-results.html b/servo/templates/shipments/add_to_return-results.html
new file mode 100755
index 0000000..0f1d94e
--- /dev/null
+++ b/servo/templates/shipments/add_to_return-results.html
@@ -0,0 +1,8 @@
+{% load i18n %}
+<ul class="nav nav-pills nav-stacked">
+{% for i in results %}
+ <li><a href="{% url 'shipments-add_to_return' pk=shipment part=i.pk %}">{{ i.order_item.code }}</a></li>
+{% empty %}
+ <li class="text-center">{% trans "No parts found" %}</li>
+{% endfor %}
+</ul>
diff --git a/servo/templates/shipments/add_to_return.html b/servo/templates/shipments/add_to_return.html
new file mode 100755
index 0000000..c1ca9da
--- /dev/null
+++ b/servo/templates/shipments/add_to_return.html
@@ -0,0 +1,14 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+{% trans "Add part to return" %}
+{% endblock header %}
+
+{% block body %}
+<form method="post" action="{{ action }}" accept-charset="utf-8" data-target="#search-results" id="search-form">
+ {% csrf_token %}
+ <input type="text" class="search-query" name="q" autocomplete="off" placeholder="{% trans "Return order number" %}"/>
+</form>
+<div id="search-results"></div>
+{% endblock body %}
diff --git a/servo/templates/shipments/edit_bulk_return.html b/servo/templates/shipments/edit_bulk_return.html
new file mode 100755
index 0000000..588a998
--- /dev/null
+++ b/servo/templates/shipments/edit_bulk_return.html
@@ -0,0 +1,101 @@
+{% extends "shipments/list_returns.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block toolbar %}
+ <a href="{% url 'shipments-pick_for_return' shipment.pk %}" class="btn" data-modal="#modal"><i class="icon-plus"></i> {% trans "Add Part" %}</a>
+<!--
+ {% if formset|length %}
+ <a href="{% url 'shipments-verify' shipment.pk %}" class="btn"><i class="icon-ok"></i> {% trans "Verify" %}</a>
+ {% else %}
+ <a href="#" class="btn disabled"><i class="icon-ok"></i> {% trans "Verify" %}</a>
+ {% endif %}
+//-->
+{% endblock toolbar %}
+
+{% block second_column %}
+<ul class="nav nav-tabs">
+ {% for k, v in accounts %}
+ <li class="{% active request k %}"><a href="{% url 'shipments-edit_bulk_return' ship_to=k %}">{{ v }}</a></li>
+ {% endfor %}
+</ul>
+<form method="post" action="">
+ <div class="span3">
+ {% block return_info %}
+ {% csrf_token %}
+ {{ formset.management_form }}
+ <div class="control-group">
+ <label class="control-label">{% trans "Carrier" %}</label>
+ <div class="controls">
+ {{ form.carrier }}
+ </div>
+ </div>
+ <div class="control-group">
+ <label class="control-label">{% trans "Tracking" %}</label>
+ <div class="controls">
+ {{ form.tracking_id }}
+ </div>
+ </div>
+ <div class="control-group">
+ <label class="control-label">{% trans "Dimensions" %}</label>
+ <div class="controls">
+ {{ form.length }} {{ form.width }} {{ form.height }}
+ </div>
+ </div>
+ <div class="control-group">
+ <label class="control-label">{% trans "Weight" %}</label>
+ <div class="controls">
+ {{ form.weight }}
+ </div>
+ </div>
+ {% endblock return_info %}
+ </div>
+ <div class="span9">
+ <table class="table">
+ <thead>
+ <th>{% trans "Part" %}</th>
+ <th>{% trans "Reference" %}</th>
+ <th style="width:100px">{% trans "Return Order" %}</th>
+ <th>{% trans "Overpack" %}</th>
+ <th></th>
+ </thead>
+ <tbody>
+ {% block return_parts %}
+ {% for f in formset %}
+ <tr>
+ {{ f.id }}
+ {{ f.part_number }}
+ {{ f.part_title }}
+ {{ f.service_order }}
+ {{ f.return_order }}
+ {% with f.instance as p %}
+ <td><strong>{{ p.part_number }}</strong><br/>{{ p.part_title }}</td>
+ {% if p.order_item %}
+ <td><a href="{{ p.order_item.order.get_absolute_url }}">{{ p.reference }}</a></td>
+ {% else %}
+ <td>{{ p.reference }}</td>
+ {% endif %}
+ <td>{{ f.return_order.value }}</td>
+ <td>{% include "snippets/control_group.html" with field=f.box_number %}</td>
+ <td><a href="{% url 'shipments-remove_from_return' shipment.pk p.pk %}" class="btn"><i class="icon-trash"></i></td>
+ {% endwith %}
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="5" class="empty muted">{% trans "No parts registered for this shipment" %}</td>
+ </tr>
+ {% endfor %}
+ {% endblock return_parts %}
+ </tbody>
+ </table>
+ {% block form_controls %}
+ <div class="form-actions">
+ <label class="checkbox pull-left">
+ <input type="checkbox" name="confirm" id="id_confirm"> {% trans "Confirm" %}
+ </label>
+ <button type="submit" class="btn pull-right spin" id="save-bulk-return" data-placeholder="{% trans "Submit" %}">{% trans "Save" %}</button>
+ </div>
+ {% endblock form_controls %}
+ </div>
+</form>
+{% endblock second_column %}
diff --git a/servo/templates/shipments/index.html b/servo/templates/shipments/index.html
new file mode 100755
index 0000000..1584f65
--- /dev/null
+++ b/servo/templates/shipments/index.html
@@ -0,0 +1,46 @@
+{% extends "products/index.html" %}
+{% load i18n %}
+{% load servo_tags %}
+
+{% block toolbar %}{% endblock toolbar %}
+
+{% block content %}
+<div class="row-fluid row-header">
+ <div class="span12">
+ <form class="form-search pull-right">
+ <div class="input-append">
+ <input type="text" class="search-query filter" placeholder="{% trans "Filter results" %}"/>
+ <button type="button" class="btn" data-toggle="collapse" data-target="#collapsable"><i class="icon-search"></i></button>
+ </div>
+ </form>
+ </div>
+</div>
+{% include "products/tabs.html" %}
+
+<div class="row-fluid">
+ {% block second_row %}
+ <div class="span3">
+ {% block first_column %}
+ <ul class="nav nav-list">
+ <li class="{% active request "incoming" %}">
+ <a href="{% url 'shipments-list_incoming' %}">{% trans "Incoming" %} <span class="badge pull-right">{{ counts.incoming }}</span></a>
+ </li>
+ <li class="{% active request "returns/pending" %}">
+ <a href="{% url 'shipments-edit_bulk_return' %}">{% trans "Parts Pending Return" %} <span class="badge pull-right">{{ counts.pending_return }}</span></a>
+ </li>
+ <li class="{% active request 'returns/list' %}"><a href="{% url 'shipments-list_bulk_returns' %}">{% trans "Browse Returns" %} <span class="badge pull-right">{{ counts.returns }}</span></a></li>
+ {% endblock first_column %}
+ </ul>
+ </div>
+ <div class="span9">
+ {% block second_column %}
+ {% endblock second_column %}
+ </div>
+ {% endblock second_row %}
+</div>
+
+{% endblock content %}
+
+{% block breadcrumb %}
+
+{% endblock breadcrumb %}
diff --git a/servo/templates/shipments/list_bulk_returns.html b/servo/templates/shipments/list_bulk_returns.html
new file mode 100755
index 0000000..f2c4843
--- /dev/null
+++ b/servo/templates/shipments/list_bulk_returns.html
@@ -0,0 +1,37 @@
+{% extends "shipments/list_returns.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block second_column %}
+<table class="table table-striped sortable">
+ <thead>
+ <tr>
+ <th>{% trans "ID" %}</th>
+ <th>{% trans "Tracking ID" %}</th>
+ <th>{% trans "Tracking URL" %}</th>
+ <th>{% trans "Parts" %}</th>
+ <th>{% trans "Returned" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for i in returns %}
+ <tr>
+ {% if i.return_id %}
+ <td><a href="{% url 'shipments-view_packing_list' pk=i.pk %}" class="window">{{ i.return_id }}</a></td>
+ {% else %}
+ <td></td>
+ {% endif %}
+ <td>{{ i.tracking_id }}</td>
+ <td><a href="{{ i.tracking_url }}">{{ i.get_carrier_display }}</a></td>
+ <td>{{ i.num_parts }}</td>
+ <td>{{ i.dispatched_by }}<br/><small class="muted">{{ i.dispatched_at|date:"SHORT_DATETIME_FORMAT" }}</small></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% include "pagination.html" with items=returns %}
+{% endblock second_column %}
+
+{% block breadcrumb %}
+<li class="active"><span class="divider">/</span> {% trans "Browse Returns" %}</li>
+{% endblock breadcrumb %}
diff --git a/servo/templates/shipments/list_incoming.html b/servo/templates/shipments/list_incoming.html
new file mode 100755
index 0000000..5d2d784
--- /dev/null
+++ b/servo/templates/shipments/list_incoming.html
@@ -0,0 +1,61 @@
+{% extends "shipments/index.html" %}
+{% load i18n %}
+
+{% block toolbar %}
+{% endblock toolbar %}
+
+{% block second_column %}
+{% include "snippets/filtering_form.html" %}
+<form method="post" action="">
+ {% csrf_token %}
+ <table class="table table-hover sortable">
+ <thead>
+ <tr>
+ {% if can_receive %}
+ <th data-defaultsort="disabled"></th>
+ {% endif %}
+ <th>{% trans "Part" %}</th>
+ <th>{% trans "Service Order" %}</th>
+ <th>{% trans "Confirmation" %}</th>
+ <th>{% trans "Ordered" %}</th>
+ </tr>
+ </thead>
+ <tbody class="searchable">
+ {% for i in inventory %}
+ <tr>
+ {% if can_receive %}
+ <td><input type="checkbox" name="id" value="{{ i.pk|safe }}" class="toggle-submit"/></td>
+ {% endif %}
+ {% with i.product as p %}
+ <td data-value="{{ p.code }}">
+ <strong><a href="{% url 'shipments-view_incoming' i.pk %}" data-modal="#modal">{{ p.code }}</a></strong><br/>{{ p.title }}
+ </td>
+ {% endwith %}
+ {% with i.purchase_order as po %}
+ <td data-value="{{ po.sales_order.code }}">
+ {% if po.sales_order %}
+ <a href="{% url 'orders-edit' po.sales_order.pk %}">{{ po.sales_order.code }}</a>
+ {% endif %}
+ <br/><small class="muted">{{ po.reference }}</small>
+ </td>
+ <td>{{ po.confirmation }}</td>
+ <td>{{ po.created_by }}<br/><small class="muted">{{ po.submitted_at|date:"SHORT_DATE_FORMAT" }}</small></td>
+ {% endwith %}
+ </tr>
+ {% empty %}
+ <tr><td colspan="7" class="muted empty">{% trans "No incoming products" %}</td></tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% if can_receive %}
+ <div class="form-actions">
+ <button type="submit" class="btn btn-primary pull-right" disabled="disabled">{% trans "Receive" %}</button>
+ </div>
+ {% endif %}
+</form>
+{% include "pagination.html" with items=inventory %}
+{% endblock second_column %}
+
+{% block breadcrumb %}
+<li class="active"><span class="divider">/</span><a href="{% url 'shipments-list_incoming' %}">{% trans "Incoming" %}</a></li>
+{% endblock breadcrumb %}
diff --git a/servo/templates/shipments/list_returns.html b/servo/templates/shipments/list_returns.html
new file mode 100755
index 0000000..7d57286
--- /dev/null
+++ b/servo/templates/shipments/list_returns.html
@@ -0,0 +1,44 @@
+{% extends "shipments/index.html" %}
+{% load servo_tags %}
+{% load i18n %}
+
+{% block second_column %}
+<form method="post" action="">
+ {% csrf_token %}
+ <table class="table table-hover">
+ <thead>
+ <tr>
+ <th><input type="checkbox" class="toggle_column"/></th>
+ <th>{% trans "Code" %}</th>
+ <th>{% trans "Title" %}</th>
+ <th>{% trans "Order" %}</th>
+ <th>{% trans "Return Order" %}</th>
+ <th>{% trans "Register For Return" %}</th>
+ <th>{% trans "Serial Number" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for p in parts %}
+ <tr>
+ <td><input type="checkbox" name="items" value="{{ p.returnOrderNumber }}"/></td>
+ <td>{{ p.partNumber }}</td>
+ <td>{{ p.partDescription|truncatechars:42 }}</td>
+ <td>{{ p.purchaseOrderNumber }}</td>
+ <td><a href="{% url 'shipments-return_label' p.partNumber p.returnOrderNumber %}" class="window">{{ p.returnOrderNumber }}</a></td>
+ <td>{{ p.registeredForReturn }}</td>
+ <td>{{ p.kbbSerialNumber }}</td>
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="7" class="muted empty">{% trans "No parts pending return" %}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <button type="submit" class="btn btn-primary pull-right" disabled="disabled">{% trans "Submit" %}</button>
+</form>
+{% endblock second_column %}
+
+{% block breadcrumb %}
+<li class="active"><span class="divider">/</span> <a href="{% url 'shipments-returns' %}">{% trans "Parts Pending Return" %}</a></li>
+{% endblock breadcrumb %}
diff --git a/servo/templates/shipments/submit_bulk_return.html b/servo/templates/shipments/submit_bulk_return.html
new file mode 100755
index 0000000..ee897c4
--- /dev/null
+++ b/servo/templates/shipments/submit_bulk_return.html
@@ -0,0 +1,15 @@
+{% extends "modal.html" %}
+{% load i18n %}
+{% block header %}
+ {% trans "Submit the bulk return?" %}
+{% endblock header %}
+
+{% block body %}
+
+{% endblock body %}
+
+{% block footer %}
+ <form method="post" action="{{ action }}">
+ <button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
+ </form>
+{% endblock footer %}
diff --git a/servo/templates/shipments/update_part.html b/servo/templates/shipments/update_part.html
new file mode 100755
index 0000000..8c0d266
--- /dev/null
+++ b/servo/templates/shipments/update_part.html
@@ -0,0 +1,14 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {{ title }}
+{% endblock header %}
+
+{% block body %}
+{{ msg }}
+<form action="{{ action }}" method="post" class="form-horizontal">
+ {% csrf_token %}
+ {% include "form_snippet.html" %}
+</form>
+{% endblock body %}
diff --git a/servo/templates/shipments/view_bulk_return.html b/servo/templates/shipments/view_bulk_return.html
new file mode 100755
index 0000000..e5b935c
--- /dev/null
+++ b/servo/templates/shipments/view_bulk_return.html
@@ -0,0 +1,35 @@
+{% extends "shipments/edit_bulk_return.html" %}
+{% load i18n %}
+
+{% block return_info %}
+<dl>
+ <dt>{% trans "Carrier" %}</dt>
+ <dd>{{ shipment.get_carrier_display }}</dd>
+ <dt>{% trans "Tracking" %}</dt>
+ <dd>{{ shipment.tracking_id }}</dd>
+ <dt>{% trans "Dimensions" %}</dt>
+ <dd>{{ shipment.length }} x {{ shipment.width }} x {{ shipment.height }}</dd>
+ <dt>{% trans "Weight" %}</dt>
+ <dd>{{ shipment.weight }} kg</dd>
+</dl>
+<a class="btn window" href="{{ shipment.packing_list.url }}"><i class="icon-file"></i> {% trans "Open Packing List" %}</a>
+{% endblock return_info %}
+
+{% block return_parts %}
+{% for p in shipment.servicepart_set.all %}
+<tr>
+ <td><strong>{{ p.part_number }}</strong><br/>{{ p.part_title }}</td>
+ <td><a href="{% url 'orders-edit' p.order_item.order.pk %}">{{ p.service_order }}</a></td>
+ <td><a href="{% url 'parts-return_label' p.repair_id p.pk %}">{{ p.return_order }}</a></td>
+ <td>{{ p.box_number|default:"Individual" }}</td>
+</tr>
+{% empty %}
+<tr>
+ <td colspan="6" class="empty muted">{% trans "No parts registered for this shipment" %}</td>
+</tr>
+{% endfor %}
+{% endblock return_parts %}
+
+{% block form_controls %}
+
+{% endblock form_controls %}
diff --git a/servo/templates/snippets/alert.html b/servo/templates/snippets/alert.html
new file mode 100644
index 0000000..9cc4f50
--- /dev/null
+++ b/servo/templates/snippets/alert.html
@@ -0,0 +1 @@
+<div class="alert alert-{{ kind }}" role="alert">{{ message }}</div>
diff --git a/servo/templates/snippets/control_group.html b/servo/templates/snippets/control_group.html
new file mode 100755
index 0000000..7e61d6a
--- /dev/null
+++ b/servo/templates/snippets/control_group.html
@@ -0,0 +1,3 @@
+<div class="control-group{% for e in field.errors %} error{% endfor %}">
+ {{ field }}
+</div>
diff --git a/servo/templates/snippets/dropdown.html b/servo/templates/snippets/dropdown.html
new file mode 100755
index 0000000..70ee226
--- /dev/null
+++ b/servo/templates/snippets/dropdown.html
@@ -0,0 +1,11 @@
+<div class="btn-group">
+ <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+ <span class="caret"></span>
+ {{ menu.title }}
+ </a>
+ <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu">
+ {% for i in menu.items %}
+ <li><a tabindex="-1" href="{{ i.href }}">{{ i.title }}</a></li>
+ {% endfor %}
+ </ul>
+</div>
diff --git a/servo/templates/snippets/dropdown_menu.html b/servo/templates/snippets/dropdown_menu.html
new file mode 100755
index 0000000..c57b563
--- /dev/null
+++ b/servo/templates/snippets/dropdown_menu.html
@@ -0,0 +1,5 @@
+<ul class="dropdown-menu">
+{% for i in items %}
+ <li><a href="{{ i.url }}">{{ i.title }}</a></li>
+{% endfor %}
+</ul>
diff --git a/servo/templates/snippets/error_modal.html b/servo/templates/snippets/error_modal.html
new file mode 100755
index 0000000..c63010a
--- /dev/null
+++ b/servo/templates/snippets/error_modal.html
@@ -0,0 +1,10 @@
+{% extends "modal.html" %}
+{% load i18n %}
+
+{% block header %}
+ {% trans "An error occured..." %}
+{% endblock header %}
+
+{% block body %}
+ {{ error }}
+{% endblock body %}
diff --git a/servo/templates/snippets/filtering_form.html b/servo/templates/snippets/filtering_form.html
new file mode 100755
index 0000000..b2ce391
--- /dev/null
+++ b/servo/templates/snippets/filtering_form.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+<div id="collapsable" class="collapse out">
+ <form method="post" action="" class="form-inline">{% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label|default:"&nbsp;" }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <p class="clearfix"><hr/></p>
+ <div class="pull-right">
+ <button type="submit" class="btn btn-primary"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </form>
+</div>
diff --git a/servo/templates/snippets/form_field.html b/servo/templates/snippets/form_field.html
new file mode 100755
index 0000000..59578f4
--- /dev/null
+++ b/servo/templates/snippets/form_field.html
@@ -0,0 +1,12 @@
+<div class="control-group{% for e in field.errors %} error{% endfor %}">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">
+ {{ field }}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ {% for e in field.errors %}
+ <span class="help-inline">{{ e }}</span>
+ {% endfor %}
+ </div>
+</div>
diff --git a/servo/templates/snippets/form_field_label.html b/servo/templates/snippets/form_field_label.html
new file mode 100755
index 0000000..0a8d4b1
--- /dev/null
+++ b/servo/templates/snippets/form_field_label.html
@@ -0,0 +1 @@
+<label class="control-label {{ field.css_classes }}">{% trans field.label %}</label>
diff --git a/servo/templates/snippets/form_input.html b/servo/templates/snippets/form_input.html
new file mode 100755
index 0000000..1d686d0
--- /dev/null
+++ b/servo/templates/snippets/form_input.html
@@ -0,0 +1,22 @@
+{% load i18n %}
+{% load servo_tags %}
+
+<div class="control-group{% for e in field.errors %} error{% endfor %}">
+ {% if field|widget_is:"CheckboxInput" %}
+ <div class="controls">
+ <label class="checkbox">
+ {{ field }} {% trans field.label %}
+ </label>
+ </div>
+ {% else %}
+ <div class="controls">
+ {{ field }}
+ {% if field.help_text %}
+ <span class="help-block">{% trans field.help_text %}</span>
+ {% endif %}
+ {% for e in field.errors %}
+ <span class="help-inline">{{ e }}</span>
+ {% endfor %}
+ </div>
+ {% endif %}
+</div>
diff --git a/servo/templates/snippets/modal.html b/servo/templates/snippets/modal.html
new file mode 100755
index 0000000..dfa552a
--- /dev/null
+++ b/servo/templates/snippets/modal.html
@@ -0,0 +1,14 @@
+{% load i18n %}
+<div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal">×</button>
+ <h3>{% block header %}{% endblock header %}</h3>
+</div>
+<div class="modal-body">
+ {% block body %}{% endblock body %}
+</div>
+<div class="modal-footer">
+{% block footer %}
+ <button class="btn" data-dismiss="modal">{% trans "Close" %}</button>
+ <button type="submit" class="btn btn-primary submit">{% trans "Done" %}</button>
+{% endblock footer %}
+</div>
diff --git a/servo/templates/stats/index.html b/servo/templates/stats/index.html
new file mode 100755
index 0000000..2edc57d
--- /dev/null
+++ b/servo/templates/stats/index.html
@@ -0,0 +1,68 @@
+{% extends "default.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<ul class="nav nav-tabs" style="margin-top:30px">
+{% block tabs %}
+ <li class="active"><a href="{% url 'stats-index' %}">{% trans "Technicians" %}</a></li>
+ <li><a href="{% url 'stats-locations' %}">{% trans "Locations" %}</a></li>
+ <li><a href="{% url 'stats-queues' %}">{% trans "Queues" %}</a></li>
+ <li><a href="{% url 'stats-repairs' %}">{% trans "Repairs" %}</a></li>
+ <li><a href="{% url 'stats-statuses' %}">{% trans "Statuses" %}</a></li>
+ <li><a href="{% url 'stats-sales' %}">{% trans "Sales" %}</a></li>
+{% endblock tabs %}
+</ul>
+
+<div class="row-fluid">
+ <div class="span12 well">
+ {% block filter_block %}
+ <form action="#" method="post" class="form-inline" id="stats-form">
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <div class="control-group">
+ <label class="control-label">&nbsp;</label>
+ <div class="controls">
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </div>
+ </form>
+ {% endblock filter_block %}
+ </div>
+</div>
+<div class="row-fluid">
+ <div class="span12">
+ {% block stats %}
+ <h2>{% trans "Orders Assigned" %}</h2>
+ <p>{% trans "Shows how many new orders have been assigned to each technician over the given time period." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/orders/runrate/" %}
+ <hr/>
+ <h2>{% trans "Orders Created" %}</h2>
+ <p>{% trans "This graph shows how many orders are checked in by each user." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/created/user/" %}
+ <hr/>
+ <h2>{% trans "Work Distribution" %}</h2>
+ <p>{% trans "Shows you how the total number of service orders is distributed across the technicians at this location." %}</p>
+ <div class="plot plot-pie pull-left" data-source="/stats/data/orders/techs/"></div>
+ {% endblock stats %}
+ </div>
+</div>
+
+{% endblock content %}
+
+{% block crumbs %}
+<li class="active">{% trans "Statistics" %}</li>
+{% endblock crumbs %}
+
+{% block media %}
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.min.js"></script>
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.time.min.js"></script>
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.pie.min.js"></script>
+<script src="{{ STATIC_URL }}js/flot/jquery.flot.resize.min.js"></script>
+<script src="{{ STATIC_URL }}js/stats.js" type="text/javascript"></script>
+{% endblock media %}
diff --git a/servo/templates/stats/locations.html b/servo/templates/stats/locations.html
new file mode 100755
index 0000000..576da8b
--- /dev/null
+++ b/servo/templates/stats/locations.html
@@ -0,0 +1,33 @@
+{% extends "stats/index.html" %}
+{% load i18n %}
+
+{% block tabs %}
+ <li><a href="{% url 'stats-index' %}">{% trans "Technicians" %}</a></li>
+ <li class="active"><a href="{% url 'stats-index' %}">{% trans "Locations" %}</a></li>
+ <li><a href="{% url 'stats-queues' %}">{% trans "Queues" %}</a></li>
+ <li><a href="{% url 'stats-repairs' %}">{% trans "Repairs" %}</a></li>
+ <li><a href="{% url 'stats-statuses' %}">{% trans "Statuses" %}</a></li>
+ <li><a href="{% url 'stats-sales' %}">{% trans "Sales" %}</a></li>
+{% endblock tabs %}
+
+{% block stats %}
+ <h2>{% trans "Orders Created" %}</h2>
+ <p>{% trans "Shows you how many orders are created at each location." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/created/location/" %}
+ <hr/>
+ <h2>{% trans "Orders Closed" %}</h2>
+ <p>{% trans "Shows you how many orders have been closed at each location." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/closed/location/" %}
+ <hr/>
+ <h2>{% trans "Average Turnaround" %}</h2>
+ <p>{% trans "Shows how many hours it takes to complete an order at each location." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/turnaround/location/" %}
+ <hr/>
+ <h2>{% trans "Average Runrate" %}</h2>
+ <p>{% trans "Shows you how many orders people are working on at each location." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/runrate/location/" %}
+ <hr/>
+ <h2>{% trans "Work Distribution" %}</h2>
+ <p>{% trans "This shows you how your overall work load is distributed across your service locations." %}</p>
+ <div class="plot plot-pie pull-left" data-source="/stats/data/distribution/location/"></div>
+{% endblock stats %}
diff --git a/servo/templates/stats/newstats.html b/servo/templates/stats/newstats.html
new file mode 100644
index 0000000..1900efc
--- /dev/null
+++ b/servo/templates/stats/newstats.html
@@ -0,0 +1,87 @@
+{% extends "default.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<ul class="nav nav-tabs" style="margin-top:30px">
+{% block tabs %}
+ <li><a href="{% url 'stats-index' %}">{% trans "Technicians" %}</a></li>
+ <li><a href="{% url 'stats-locations' %}">{% trans "Locations" %}</a></li>
+ <li><a href="{% url 'stats-queues' %}">{% trans "Queues" %}</a></li>
+ <li class="active"><a href="{% url 'stats-repairs' %}">{% trans "Repairs" %}</a></li>
+ <li><a href="{% url 'stats-statuses' %}">{% trans "Statuses" %}</a></li>
+ <li><a href="{% url 'stats-sales' %}">{% trans "Sales" %}</a></li>
+{% endblock tabs %}
+</ul>
+
+<div class="row-fluid">
+ <div class="span12 well">
+ {% block filter_block %}
+ <form action="#" method="get" class="form-inline" id="stats-form">
+ {% for field in form %}
+ <div class="control-group pull-left">
+ <label class="control-label">{{ field.label }}</label>
+ <div class="controls">{{ field }}</div>
+ </div>
+ {% endfor %}
+ <div class="control-group">
+ <label class="control-label">&nbsp;</label>
+ <div class="controls">
+ <button class="btn btn-primary pull-right" type="submit"><i class="icon-search icon-white"></i> {% trans "Search" %}</button>
+ </div>
+ </div>
+ </form>
+ {% endblock filter_block %}
+ </div>
+</div>
+<div class="row-fluid">
+ <div class="span12">
+ {% block stats %}
+ <table class="table table-hover sortable">
+ <thead>
+ <tr>
+ <th>{% trans "Technician" %}</th>
+ <th>{% trans "Cases Created" %}</th>
+ <th>{% trans "Cases Assigned" %}</th>
+ <th>{% trans "Repairs Created" %}</th>
+ <th>{% trans "Cases Dispatched" %}</th>
+ <td></td>
+ </tr>
+ </thead>
+ <tbody>
+ {% for r in results %}
+ <tr>
+ <td>{{ r.name }}</td>
+ <td>{{ r.created }}</td>
+ <td>{{ r.assigned }}</td>
+ <td>{{ r.repairs }}</td>
+ <td>{{ r.dispatched }}</td>
+ <td></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ <tfoot style="font-weight:bold">
+ <tr>
+ <td>{% trans "Total" %}</td>
+ <td>{{ totals.created }}</td>
+ <td>{{ totals.assigned }}</td>
+ <td>{{ totals.repairs }}</td>
+ <td>{{ totals.dispatched }}</td>
+ <td>{{ totals.diff }}</td>
+ </tr>
+ {% if totals.turnaround.nonzero %}
+ <tr>
+ <td>{% trans "Average turnaround time" %}</td>
+ <td colspan="5">{{ totals.turnaround.days }} {% trans "days" %}, {{ totals.turnaround.hours }} {% trans "hours" %}</td>
+ </tr>
+ {% endif %}
+ </tfoot>
+ </table>
+ {% endblock stats %}
+ </div>
+</div>
+{% endblock content %}
+
+{% block crumbs %}
+ <li class="active">{% trans "Statistics" %}</li>
+{% endblock crumbs %}
diff --git a/servo/templates/stats/plot_snippet.html b/servo/templates/stats/plot_snippet.html
new file mode 100755
index 0000000..c93a41e
--- /dev/null
+++ b/servo/templates/stats/plot_snippet.html
@@ -0,0 +1,9 @@
+<div class="row-fluid">
+ <div class="span12">
+ <div class="span10">
+ <div class="plot" data-source="{{ url }}"></div>
+ <div class="legend-container"></div>
+ </div>
+ <div class="span2"></div>
+ </div>
+</div>
diff --git a/servo/templates/stats/queues.html b/servo/templates/stats/queues.html
new file mode 100755
index 0000000..eefec46
--- /dev/null
+++ b/servo/templates/stats/queues.html
@@ -0,0 +1,29 @@
+{% extends "stats/index.html" %}
+{% load i18n %}
+
+{% block tabs %}
+ <li><a href="{% url 'stats-index' %}">{% trans "Technicians" %}</a></li>
+ <li><a href="{% url 'stats-locations' %}">{% trans "Locations" %}</a></li>
+ <li class="active"><a href="{% url 'stats-queues' %}">{% trans "Queues" %}</a></li>
+ <li><a href="{% url 'stats-repairs' %}">{% trans "Repairs" %}</a></li>
+ <li><a href="{% url 'stats-statuses' %}">{% trans "Statuses" %}</a></li>
+ <li><a href="{% url 'stats-sales' %}">{% trans "Sales" %}</a></li>
+{% endblock tabs %}
+
+{% block stats %}
+ <h2>{% trans "Orders Created" %}</h2>
+ <p>{% trans "This is your total number of orders per queue in the specified time period" %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/orders/count/" %}
+ <hr/>
+ <h2>{% trans "Orders Closed" %}</h2>
+ <p>{% trans "Shows you how many orders have been closed in each queue." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/closed/queue/" %}
+ <hr/>
+ <h2>{% trans "Average Turnaround" %}</h2>
+ <p>{% trans "Shows how many hours it takes to complete an order in each queue." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/orders/turnaround/" %}
+ <hr/>
+ <h2>{% trans "Work Distribution" %}</h2>
+ <p>{% trans "This shows your total ratio of orders over the time period distributed over each queue." %}</p>
+ <div class="plot plot-pie pull-left" data-source="/stats/data/orders/queues/"></div>
+{% endblock stats %}
diff --git a/servo/templates/stats/sales.html b/servo/templates/stats/sales.html
new file mode 100755
index 0000000..186e897
--- /dev/null
+++ b/servo/templates/stats/sales.html
@@ -0,0 +1,33 @@
+{% extends "stats/index.html" %}
+{% load i18n %}
+
+{% block tabs %}
+<li><a href="{% url 'stats-index' %}">{% trans "Technicians" %}</a></li>
+<li><a href="{% url 'stats-locations' %}">{% trans "Locations" %}</a></li>
+<li><a href="{% url 'stats-queues' %}">{% trans "Queues" %}</a></li>
+<li><a href="{% url 'stats-repairs' %}">{% trans "Repairs" %}</a></li>
+<li><a href="{% url 'stats-statuses' %}">{% trans "Statuses" %}</a></li>
+<li class="active"><a href="{% url 'stats-sales' %}">{% trans "Sales" %}</a></li>
+{% endblock tabs %}
+
+{% block stats %}
+<h2>{% trans "Sales" %}</h2>
+<p>{% trans "Shows you invoice totals per queue within the selected time period." %}</p>
+{% include "stats/plot_snippet.html" with url="/stats/data/sales/invoices/" %}
+<hr/>
+<h2>{% trans "Purchases" %}</h2>
+<p>{% trans "Shows you Purchase Order totals per queue within the selected time period." %}</p>
+{% include "stats/plot_snippet.html" with url="/stats/data/sales/purchases/" %}
+<hr/>
+<h2>{% trans "Service Parts" %}</h2>
+<p>{% trans "Shows you how many parts have been ordered for each labour tier." %}</p>
+<div class="row-fluid">
+ <div class="span12">
+ <div class="span10">
+ <div class="plot plot-bar" data-source="/stats/data/sales/parts/"></div>
+ <div class="legend-container"></div>
+ </div>
+ <div class="span2"></div>
+ </div>
+</div>
+{% endblock stats %}
diff --git a/servo/templates/stats/statuses.html b/servo/templates/stats/statuses.html
new file mode 100755
index 0000000..f4fb427
--- /dev/null
+++ b/servo/templates/stats/statuses.html
@@ -0,0 +1,21 @@
+{% extends "stats/index.html" %}
+{% load i18n %}
+
+{% block tabs %}
+ <li><a href="{% url 'stats-index' %}">{% trans "Technicians" %}</a></li>
+ <li><a href="{% url 'stats-locations' %}">{% trans "Locations" %}</a></li>
+ <li><a href="{% url 'stats-queues' %}">{% trans "Queues" %}</a></li>
+ <li><a href="{% url 'stats-repairs' %}">{% trans "Repairs" %}</a></li>
+ <li class="active"><a href="{% url 'stats-statuses' %}">{% trans "Statuses" %}</a></li>
+ <li><a href="{% url 'stats-sales' %}">{% trans "Sales" %}</a></li>
+{% endblock tabs %}
+
+{% block stats %}
+ <h2>{% trans "Orders per location" %}</h2>
+ <p>{% trans "Shows the number of orders with a particular status at the selected location that have been assigned to a technician over the specified time period." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/status/location/" %}
+ <hr/>
+ <h2>{% trans "Orders per user" %}</h2>
+ <p>{% trans "Shows the number of orders with a particular status per each user at the given location that have been assigned to a technician over the specified time period." %}</p>
+ {% include "stats/plot_snippet.html" with url="/stats/data/status/tech/" %}
+{% endblock stats %}
diff --git a/servo/templates/tabbed_form.html b/servo/templates/tabbed_form.html
new file mode 100755
index 0000000..03dae90
--- /dev/null
+++ b/servo/templates/tabbed_form.html
@@ -0,0 +1,14 @@
+<ul class="nav nav-tabs">
+{% for k, v in form.tabs %}
+ <li><a href="#tab{{ forloop.counter }}" data-toggle="tab">{{ k }}</a></li>
+{% endfor %}
+</ul>
+<div class="tab-content">
+{% for k, v in form.tabs %}
+ <div class="tab-pane" id="tab{{ forloop.counter }}">
+ <fieldset>
+ {% include "form_snippet.html" with form=v %}
+ </fieldset>
+ </div>
+{% endfor %}
+</div>
diff --git a/servo/templates/three_column_layout.html b/servo/templates/three_column_layout.html
new file mode 100755
index 0000000..e529e54
--- /dev/null
+++ b/servo/templates/three_column_layout.html
@@ -0,0 +1,27 @@
+{% extends "default.html" %}
+{% block content %}
+<div class="row-fluid">
+ <div class="span12">
+ <ul class="breadcrumb">
+ {% block breadcrumb %}{% endblock breadcrumb %}
+ </ul>
+ </div>
+</div>
+<div class="row-fluid">
+ <div class="span3">
+ {% block left_column %}
+
+ {% endblock left_column %}
+ </div>
+ <div class="span7">
+ {% block middle_column %}
+
+ {% endblock middle_column %}
+ </div>
+ <div class="span2">
+ {% block right_column %}
+
+ {% endblock right_column %}
+ </div>
+</div>
+{% endblock content %}
diff --git a/servo/templates/two_column_layout.html b/servo/templates/two_column_layout.html
new file mode 100755
index 0000000..6747eda
--- /dev/null
+++ b/servo/templates/two_column_layout.html
@@ -0,0 +1,18 @@
+{% extends "default.html" %}
+{% load i18n %}
+
+{% block content %}
+<div class="row-fluid">
+ <div class="span12">{% block header_row %}{% endblock header_row %}</div>
+</div>
+<div class="row-fluid">
+ <div class="span3">
+ {% block first_column %}
+ {% endblock first_column %}
+ </div>
+ <div class="span9">
+ {% block second_column %}
+ {% endblock second_column %}
+ </div>
+</div>
+{% endblock content %}
diff --git a/servo/templatetags/__init__.py b/servo/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/templatetags/__init__.py
diff --git a/servo/templatetags/servo_tags.py b/servo/templatetags/servo_tags.py
new file mode 100644
index 0000000..b93e750
--- /dev/null
+++ b/servo/templatetags/servo_tags.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import decimal
+
+from gsxws import objectify
+
+import babel.numbers
+from datetime import timedelta
+
+from django import template
+from django.utils import safestring, timezone
+from django.utils.translation import ugettext as _
+from django.template.defaultfilters import date
+from django.contrib.humanize.templatetags.humanize import naturaltime
+
+from servo.models.common import Configuration
+
+register = template.Library()
+
+
+@register.filter
+def gsx_diags_ts(value):
+ return objectify.gsx_diags_timestamp(value)
+
+
+@register.filter
+def unread_notifications(queryset):
+ queryset = queryset.filter(action='set_status', handled_at=None)
+ return queryset.order_by('-handled_at')
+
+
+@register.filter
+def unread_messages(queryset):
+ queryset = queryset.filter(action='note_added', handled_at=None)
+ return queryset.order_by('-handled_at')
+
+
+@register.filter
+def count_or_empty(queryset):
+ if queryset.count() > 0:
+ return queryset.count()
+ return ''
+
+
+@register.filter
+def search_url(request):
+ "Returns the proper search URL"
+ prefix = request.path.split("/")[1]
+ return "/%s/search/" % prefix
+
+
+@register.filter
+def str_find(string, substr):
+ return (string.find(substr) > -1)
+
+
+@register.filter(expects_localtime=True, is_safe=False)
+def relative_date(value):
+ if value in ('', None):
+ return ''
+
+ current = timezone.now()
+
+ if (current - value) > timedelta(days=1):
+ return date(value, "SHORT_DATETIME_FORMAT")
+
+ return naturaltime(value)
+
+
+@register.filter(is_safe=True)
+def highlight(text, string):
+ result = re.sub(r'('+string+')', '<span class="highlight">\g<0></span>', text)
+ return result
+
+
+@register.filter
+def amount_in_location(obj, user):
+ """
+ Returns how many instances of this product
+ are at the current user's location
+ """
+ return obj.get_amount_stocked(user)
+
+
+@register.filter
+def is_item_complete(obj, item):
+ return obj.is_item_complete(item)
+
+
+@register.filter
+def item_completed_by(obj, item):
+ item = is_item_complete(obj, item)
+
+ try:
+ return item.checked_by.username
+ except Exception:
+ return ''
+
+
+@register.filter
+def widget_is(obj, widget):
+ try:
+ return obj.field.widget.__class__.__name__ == widget
+ except AttributeError:
+ return False
+
+
+@register.filter(is_safe=True)
+def currency(value):
+ try:
+ c = Configuration.conf('currency')
+ return babel.numbers.format_currency(decimal.Decimal(value), c)
+ except Exception:
+ return value
+
+
+@register.simple_tag
+def active(request, *args):
+ s = '/'.join([str(i) for i in args])
+ pattern = s[:-1] if s.endswith('//') else s
+ path = request.path.lstrip('/')
+ return 'active' if re.search(pattern, path) else ''
+
+
+@register.simple_tag
+def active_url(request, url):
+ return 'active' if request.path == url else ''
+
+
+@register.simple_tag
+def paginator_page(request, page):
+ query = request.GET.copy()
+ if 'page' in query.keys():
+ del query['page']
+ query['page'] = page
+ return query.urlencode()
+
+
+@register.filter
+def markdown(text):
+ import markdown
+ result = markdown.markdown(text)
+ return safestring.mark_safe(result)
+
+
+@register.filter
+def concat(str1, str2):
+ return str(str1) + str(str2)
+
+
+@register.filter
+def addspace(s):
+ return str(s).replace(',', ', ')
+
+
+@register.filter
+def device_accessories(device, order):
+ return device.get_accessories(order)
+
+
+@register.filter
+def replace(value, arg):
+ old, new = arg.split(",")
+ return value.replace(old, new)
diff --git a/servo/tests/__init__.py b/servo/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/tests/__init__.py
diff --git a/servo/tests/create_order.json b/servo/tests/create_order.json
new file mode 100644
index 0000000..c9baf4d
--- /dev/null
+++ b/servo/tests/create_order.json
@@ -0,0 +1,19 @@
+{
+ "problem": "Problem description goes here",
+ "customer": {
+ "name": "Filipp Lepalaan",
+ "email": "support@servoapp.com",
+ "city": "Helsinki",
+ "zip_code": "00500",
+ "street_address": "Somestreet 19"
+ },
+ "attachment": {
+ "name": "receipt.pdf",
+ "data": "JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAErVAgEAAHnAOMKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjExCmVuZG9iagoyIDAgb2JqCjw8IC9UeXBlIC9QYWdlIC9QYXJlbnQgMyAwIFIgL1Jlc291cmNlcyA2IDAgUiAvQ29udGVudHMgNCAwIFIgL01lZGlhQm94IFswIDAgNTk1IDg0Ml0KPj4KZW5kb2JqCjYgMCBvYmoKPDwgL1Byb2NTZXQgWyAvUERGIF0gPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9NZWRpYUJveCBbMCAwIDU5NSA4NDJdIC9Db3VudCAxIC9LaWRzIFsgMiAwIFIgXSA+PgplbmRvYmoKNyAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMyAwIFIgPj4KZW5kb2JqCjggMCBvYmoKKFVudGl0bGVkKQplbmRvYmoKOSAwIG9iagooTWFjIE9TIFggMTAuOS41IFF1YXJ0eiBQREZDb250ZXh0KQplbmRvYmoKMTAgMCBvYmoKKEZpbGlwcCBMZXBhbGFhbikKZW5kb2JqCjExIDAgb2JqCigpCmVuZG9iagoxMiAwIG9iagooVGV4dEVkaXQpCmVuZG9iagoxMyAwIG9iagooRDoyMDE1MDUwODA4MjkxOFowMCcwMCcpCmVuZG9iagoxNCAwIG9iagooKQplbmRvYmoKMTUgMCBvYmoKWyAoKSBdCmVuZG9iagoxIDAgb2JqCjw8IC9UaXRsZSA4IDAgUiAvQXV0aG9yIDEwIDAgUiAvU3ViamVjdCAxMSAwIFIgL1Byb2R1Y2VyIDkgMCBSIC9DcmVhdG9yIDEyIDAgUgovQ3JlYXRpb25EYXRlIDEzIDAgUiAvTW9kRGF0ZSAxMyAwIFIgL0tleXdvcmRzIDE0IDAgUiAvQUFQTDpLZXl3b3JkcyAxNSAwIFIKPj4KZW5kb2JqCnhyZWYKMCAxNgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDA2NDEgMDAwMDAgbiAKMDAwMDAwMDEyNSAwMDAwMCBuIAowMDAwMDAwMjY4IDAwMDAwIG4gCjAwMDAwMDAwMjIgMDAwMDAgbiAKMDAwMDAwMDEwNyAwMDAwMCBuIAowMDAwMDAwMjI5IDAwMDAwIG4gCjAwMDAwMDAzNTEgMDAwMDAgbiAKMDAwMDAwMDQwMCAwMDAwMCBuIAowMDAwMDAwNDI2IDAwMDAwIG4gCjAwMDAwMDA0NzcgMDAwMDAgbiAKMDAwMDAwMDUxMSAwMDAwMCBuIAowMDAwMDAwNTMwIDAwMDAwIG4gCjAwMDAwMDA1NTcgMDAwMDAgbiAKMDAwMDAwMDU5OSAwMDAwMCBuIAowMDAwMDAwNjE4IDAwMDAwIG4gCnRyYWlsZXIKPDwgL1NpemUgMTYgL1Jvb3QgNyAwIFIgL0luZm8gMSAwIFIgL0lEIFsgPDRlMGI4Y2UyZmEzNjdkZjU4MThjOGE4ZjBiYTczZDM1Pgo8NGUwYjhjZTJmYTM2N2RmNTgxOGM4YThmMGJhNzNkMzU+IF0gPj4Kc3RhcnR4cmVmCjgxNAolJUVPRgo="
+ },
+ "device": {
+ "sn": "11112222333344445555",
+ "description": "MacBook Pro",
+ "accessories": ["Power adapter"]
+ }
+}
diff --git a/servo/tests/test_functional.py b/servo/tests/test_functional.py
new file mode 100644
index 0000000..b20da20
--- /dev/null
+++ b/servo/tests/test_functional.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import os
+import unittest
+from selenium import webdriver
+from selenium.webdriver.common.keys import Keys
+
+
+class NewVisitorTest(unittest.TestCase):
+ def setUp(self):
+ self.url = 'http://localhost:8000/checkin/'
+ self.browser = webdriver.Firefox()
+ self.browser.implicitly_wait(3)
+ self.browser.get(self.url)
+
+ def tearDown(self):
+ self.browser.quit()
+
+ def fill_device_details(self):
+ self.browser.find_element_by_id('id_username').send_keys('username')
+ self.browser.find_element_by_id('id_password').send_keys('password')
+ self.browser.find_element_by_id("id_pop").send_keys("/tmp/image.png")
+
+ def fill_checklist(self):
+ btn = self.browser.find_element_by_class_name('btn-default')
+ btn.click()
+ btn = self.browser.find_element_by_class_name('btn-default')
+ btn.click()
+ btn = self.browser.find_element_by_class_name('btn-default')
+ btn.click()
+
+ def fill_problem_description(self):
+ problem_field = self.browser.find_element_by_id('id_issue_description')
+ problem_field.send_keys("Mary had a little lamb")
+ self.browser.find_element_by_id("id_pop").send_keys("/tmp/image.png")
+
+ def fill_contact_fields(self):
+ field = self.browser.find_element_by_id('id_fname')
+ field.send_keys('Filipp')
+ field = self.browser.find_element_by_id('id_lname')
+ field.send_keys('Lepalaan')
+ field = self.browser.find_element_by_id('id_email')
+ field.send_keys('filipp@mac.com')
+ field = self.browser.find_element_by_id('id_phone')
+ field.send_keys('358451202717')
+ field = self.browser.find_element_by_id('id_city')
+ field.send_keys('Helsinki')
+ field = self.browser.find_element_by_id('id_postal_code')
+ field.send_keys('00500')
+ field = self.browser.find_element_by_id('id_address')
+ field.send_keys('Kustaankatu 2 C 96')
+ field = self.browser.find_element_by_id('id_agree_to_terms')
+ field.click()
+
+ def _test_visitor_can_check_status(self):
+ btn = self.browser.find_element_by_class_name('btn-default')
+ btn.click()
+ field = self.browser.find_element_by_id('id_code')
+ field.send_keys("12019537")
+ field.send_keys(Keys.ENTER)
+ self.assertEqual("Repair Status", self.browser.title)
+
+ def _test_can_checkin_wo_sn(self):
+ self.browser.find_element_by_class_name('btn-primary').click()
+ # customer has no serial number, chooses device
+ self.browser.find_element_by_id('id_choose_device').click()
+ self.browser.find_element_by_id('id_macbookpro').click()
+ self.fill_device_details()
+ self.next()
+ self.fill_problem_description()
+ self.next()
+ #self.fill_checklist()
+ #self.next()
+ self.fill_contact_fields()
+ # Submit the repair
+ self.next()
+ # Check that it actually worked
+ btn = self.browser.find_element_by_class_name('btn-large')
+ self.assertEqual(btn.text, 'Print')
+
+ def test_can_checkin_sn(self):
+ # Customer logs in to check-in
+ self.assertIn('Service Order Check-In', self.browser.title)
+
+ # customer checks warranty status...
+ snfield = self.browser.find_element_by_id('id_sn')
+ snfield.send_keys('C02GV550DJWV')
+ snfield.send_keys(Keys.ENTER)
+ self.fill_device_details()
+
+ # ... enters problem description
+ self.fill_problem_description()
+
+ # Customer fills out her contact details
+ self.fill_contact_fields()
+
+ # Customer fills in the condition
+ field = self.browser.find_element_by_id('id_condition')
+ field.send_keys('Like new')
+
+ # Submit the repair
+ submit_button = self.browser.find_element_by_id('id_btn_submit')
+ submit_button.click()
+
+ # Check that it actually worked
+ btn = self.browser.find_element_by_class_name('btn-large')
+ self.assertEqual(btn.text, 'Print')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/servo/tests/test_unit.py b/servo/tests/test_unit.py
new file mode 100755
index 0000000..e758798
--- /dev/null
+++ b/servo/tests/test_unit.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import unittest
+from django.test import TestCase
+from django.http import HttpRequest
+from django.core.urlresolvers import resolve
+from django.test.simple import DjangoTestSuiteRunner
+
+from servo.views import checkin
+
+
+class NoDbTestRunner(DjangoTestSuiteRunner):
+ """ A test runner to test without database creation """
+
+ def setup_databases(self, **kwargs):
+ """ Override the database creation defined in parent class """
+ pass
+
+ def teardown_databases(self, old_config, **kwargs):
+ """ Override the database teardown defined in parent class """
+ pass
+
+
+class ApiTest(TestCase):
+ pass
+
+
+class CheckinTest(TestCase):
+ def test_checkin_url_resolves(self):
+ found = resolve('/checkin/')
+ self.assertEqual(found.func, checkin.home)
+
+ def test_homepage_error_without_cookies(self):
+ request = HttpRequest()
+ response = checkin.home(request)
+ self.assertTrue(response.content.startswith("<!DOCTYPE html>"), response.content)
+ self.assertIn('<title>An error occurred', response.content)
+ self.assertTrue(response.content.endswith('</html>'))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/servo/urls/__init__.py b/servo/urls/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/urls/__init__.py
diff --git a/servo/urls/account.py b/servo/urls/account.py
new file mode 100644
index 0000000..13214c7
--- /dev/null
+++ b/servo/urls/account.py
@@ -0,0 +1,45 @@
+from django.conf.urls import patterns, url
+from django.views.generic import RedirectView
+
+urlpatterns = patterns(
+ "servo.views.account",
+
+ url(r'^$', RedirectView.as_view(url='orders', permanent=False)),
+
+ url(r'^search/$', "search", name="accounts-search"),
+ url(r'^orders/$', "orders", name="accounts-list_orders"),
+ url(r'^settings/$', 'settings', name="accounts-settings"),
+ url(r'^stats/$', 'stats', name="accounts-stats"),
+ url(r'^updates/$', 'updates', name="accounts-updates"),
+
+ url(r'^calendars/$', "calendars", name="calendars-list"),
+ url(r'^calendars/new/$', "edit_calendar", name="calendars-create"),
+ url(r'^calendars/(?P<pk>\d+)/$', "view_calendar", {'view': 'week'},
+ name='calendars.view'),
+
+ url(r'^calendars/(?P<pk>\d+)/delete/$', "delete_calendar",
+ name='calendars-delete'),
+
+ url(r'^calendars/(?P<pk>\d+)/(?P<view>[a-z]+)/$', "view_calendar",
+ name='calendars.view'),
+ url(r'^calendars/(?P<pk>\d+)/(?P<view>[a-z]+)/(?P<start_date>[0-9\-]+)/$', "view_calendar",
+ name='calendars-view_calendar'),
+ url(r'^calendars/(?P<pk>\d+)/(?P<view>[a-z]+)/download/$', "download_calendar",
+ name='calendars-download'),
+ url(r'^calendars/(?P<pk>\d+)/(?P<view>[a-z]+)/(?P<start_date>[0-9\-]+)/print/$', "print_calendar",
+ name="calendars-print"),
+ url(r'^calendars/(?P<pk>\d+)/(?P<view>[a-z]+)/edit/$', "edit_calendar",
+ name='calendars-edit'),
+
+ url(r'^calendars/(?P<cal_pk>\d+)/events/new/$', "edit_calendar_event",
+ name='calendars.event.edit'),
+ url(r'^calendars/(?P<cal_pk>\d+)/events/(?P<pk>\d+)/edit/$', "edit_calendar_event",
+ name='calendars.event.edit'),
+ url(r'^calendars/(?P<cal_pk>\d+)/events/(?P<pk>\d+)/delete/$', "delete_calendar_event",
+ name='calendars.event.delete'),
+ url(r'^calendars/(?P<cal_pk>\d+)/events/(?P<pk>\d+)/finish/$', "finish_calendar_event",
+ name='calendars.event.finish'),
+
+ url(r'^notifications/clear/$', "clear_notifications", name="accounts-clear_notifications"),
+
+)
diff --git a/servo/urls/admin.py b/servo/urls/admin.py
new file mode 100644
index 0000000..2c8b1ca
--- /dev/null
+++ b/servo/urls/admin.py
@@ -0,0 +1,76 @@
+from django.conf.urls import patterns, url, include
+from servo.views import admin
+
+
+urlpatterns = patterns(
+ 'servo.views.admin',
+ url(r'^settings/$', 'settings', name='admin-settings'),
+
+ url(r'^statuses/$', 'statuses', name='admin-statuses'),
+ url(r'^statuses/new/$', 'edit_status', name="admin-create_status"),
+ url(r'^statuses/(\d+)/edit/$', 'edit_status', name="admin-edit_status"),
+ url(r'^statuses/(\d+)/delete/$', 'remove_status', name="admin-delete_status"),
+
+ url(r'^users/$', 'list_users', name='admin-list_users'),
+ url(r'^users/new/$', 'edit_user', name="admin-create_user"),
+ url(r'^users/upload/$', 'upload_users', name="admin-upload_users"),
+ url(r'^users/(\d+)/edit/$', 'edit_user', name="admin-edit_user"),
+ url(r'^users/(\d+)/delete/$', 'delete_user', name="admin-delete_user"),
+ url(r'^users/(\d+)/delete_tokens/$', 'delete_user_token', name="admin-delete_user_token"),
+ url(r'^users/(\d+)/create_token/$', 'create_user_token', name="admin-create_user_token"),
+
+ url(r'^groups/$', 'list_groups', name='admin-list_groups'),
+ url(r'^groups/new/$', 'edit_group', name="admin-create_group"),
+ url(r'^groups/(\d+)/edit/$', 'edit_group', name="admin-edit_group"),
+ url(r'^groups/(\d+)/delete/$', 'delete_group', name="admin-delete_group"),
+
+ url(r'^tags/$', 'tags', name='admin-tags'),
+ url(r'^tags/(?P<type>[a-z]+)/$', 'tags', name='admin-tags'),
+ url(r'^tags/(?P<type>[a-z]+)/new/$', 'edit_tag', name="admin-create_tag"),
+ url(r'^tags/[a-z]+/(?P<pk>\d+)/delete/$', 'delete_tag', name="admin-delete_tag"),
+ url(r'^tags/(?P<type>[a-z]+)/(?P<pk>\d+)/$', 'edit_tag', name="admin-edit_tag"),
+
+ url(r'^fields/(?P<type>[a-z]+)/$', 'fields', name='admin-fields'),
+ url(r'^fields/(?P<type>[a-z]+)/new/$', 'edit_field', name="admin-create_field"),
+ url(r'^fields/[a-z]+/(\d+)/delete/$', 'delete_field', name="admin-delete_field"),
+ url(r'^fields/(?P<type>[a-z]+)/(?P<pk>\d+)/edit/$', 'edit_field', name="admin-edit_field"),
+
+ url(r'^templates/$', 'list_templates', name='admin-list_templates'),
+ url(r'^templates/new/$', 'edit_template', name='admin-edit_template'),
+ url(r'^templates/(\d+)/edit/$', 'edit_template', name='admin-edit_template'),
+ url(r'^templates/(\d+)/delete/$', 'delete_template', name='admin-delete_template'),
+
+ url(r'^queues/$', 'queues', name='admin-queues'),
+ #url(r'^queues/$', admin.QueueListView.as_view(), name='admin-queues'),
+ url(r'^queues/new/$', 'edit_queue', name="admin-create_queue"),
+ url(r'^queues/(?P<pk>\d+)/edit/$', 'edit_queue', name="admin-edit_queue"),
+ #url(r'^queues/(?P<pk>\d+)/edit/$', admin.QueueListView.as_view(), name='admin-edit_queue'),
+ url(r'^queues/(\d+)/delete/$', 'delete_queue', name="admin-delete_queue"),
+
+ url(r'^gsx/accounts/$', 'list_gsx_accounts', name='admin-list_gsx_accounts'),
+ url(r'^gsx/accounts/new/$', 'edit_gsx_account', name='admin-edit_gsx_account'),
+ url(r'^gsx/accounts/(\d+)/$', 'edit_gsx_account', name='admin-edit_gsx_account'),
+ url(r'^gsx/accounts/(\d+)?/delete/$', 'delete_gsx_account', name='admin-delete_gsx_account'),
+
+ url(r'^locations/$', 'locations', name='admin-locations'),
+ url(r'^locations/new/$', 'edit_location', name='admin-create_location'),
+ url(r'^locations/(\d+)/edit/$', 'edit_location', name='admin-edit_location'),
+ url(r'^locations/(\d+)/delete/$', 'delete_location', name='admin-delete_location'),
+
+ url(r'^notifications/$', 'notifications', name='admin-notifications'),
+ url(r'^notifications/(\w+)/$', 'edit_notification'),
+
+ url(r'^checklists/$', 'checklists', name='admin-checklists'),
+ url(r'^checklists/new/$', 'edit_checklist', name='admin-create_checklist'),
+ url(r'^checklists/(?P<pk>\d+)/edit/$', 'edit_checklist', name='admin-edit_checklist'),
+ url(r'^checklists/(?P<pk>\d+)/delete/$', 'delete_checklist', name='admin-delete_checklist'),
+
+ url(r'^sites/$', 'list_sites', name="admin-list_sites"),
+ url(r'^sites/new/$', 'edit_site', name="admin-create_site"),
+ url(r'^sites/(\d+)/edit/$', 'edit_site', name="admin-edit_site"),
+
+ url(r'^rules/', include('servo.urls.rules')),
+
+ url(r'^backups/$', 'backups', name="admin-backups"),
+
+)
diff --git a/servo/urls/api.py b/servo/urls/api.py
new file mode 100644
index 0000000..462a692
--- /dev/null
+++ b/servo/urls/api.py
@@ -0,0 +1,34 @@
+from django.conf.urls import patterns, url, include
+
+from servo.views import api
+
+
+urlpatterns = patterns(
+ "servo.views.api",
+ url(r'^status/$', api.OrderStatusView.as_view(), name='api-status'),
+ url(r'^tags/$', 'tags', name='api-tags'),
+ url(r'^users/$', 'users', name='api-users'),
+ url(r'^queues/$', 'queues', name='api-queues'),
+ url(r'^places/$', 'places', name='api-places'),
+ url(r'^locations/$', 'locations', name='api-locations'),
+ url(r'^statuses/$', 'statuses', name='api-statuses'),
+
+ url(r'^orders/$', 'orders', name='api-order_create'),
+ url(r'^orders/(\d{8})/$', 'orders', name='api-order_list'),
+ url(r'^orders/(?P<pk>\d+)/$', 'orders', name='api-order_detail'),
+
+ url(r'^warranty/$', 'warranty', name='api-device_warranty'),
+ url(r'^messages/$', 'messages', name='api-messages'),
+ url(r'^device_models/$', 'device_models'),
+
+ url(r'^status/(?P<pk>\d+)/$', 'order_status', name='queuestatus-detail'),
+ url(r'^notes/(?P<pk>\d+)/$', 'notes', name='api-note_detail'),
+ url(r'^orders/products/(?P<pk>\d+)/$', 'order_items', name='api-order_items'),
+
+ url(r'^users/(?P<pk>\d+)/$', 'user_detail', name='api-user_detail'),
+
+ url(r'^customers/$', 'customers', name='api-customers'),
+ url(r'^customers/(?P<pk>\d+)/$', 'customers', name='api-customer_detail'),
+
+ url(r'^devices/(?P<pk>\d+)/$', 'devices', name='api-device_detail'),
+)
diff --git a/servo/urls/checkin.py b/servo/urls/checkin.py
new file mode 100644
index 0000000..a05e306
--- /dev/null
+++ b/servo/urls/checkin.py
@@ -0,0 +1,15 @@
+from django.conf.urls import patterns, url
+from servo.views.checkin import *
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', index, name='checkin-index'),
+ url(r'^customer/$', get_customer, name='checkin-get_customer'),
+ url(r'^reset/$', reset, name='checkin-reset'),
+ url(r'^status/$', status, name='checkin-status'),
+ url(r'^checkin/print/(\w+)/$', print_confirmation, name='checkin-print'),
+ url(r'^thanks/(\w+)/$', thanks, name='checkin-thanks'),
+ url(r'^terms/$', terms, name='checkin-terms'),
+
+)
diff --git a/servo/urls/customer.py b/servo/urls/customer.py
new file mode 100644
index 0000000..86c8e9d
--- /dev/null
+++ b/servo/urls/customer.py
@@ -0,0 +1,29 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ "servo.views.customer",
+ url(r'^$', 'index', {'group': 'all'}, name="customers-list_all"),
+ url(r'^find/$', 'find', name="customers-find"),
+ url(r'^search/$', 'search', name="customers-search"),
+ url(r'^filter/$', 'filter', name="customers-filter"),
+ url(r'^download/$', 'download', name="customers-download"),
+ url(r'^download/(?P<group>[\w\-]+)/$', 'download', name="customers-download"),
+ url(r'^find/download$', 'download', name="customers-download_search"),
+ url(r'^groups/add/$', 'edit_group', name="customers-create_group"),
+ url(r'^groups/(?P<group>[\w\-]+)/edit/$', 'edit_group', name="customers-edit_group"),
+ url(r'^groups/(?P<group>[\w\-]+)/delete/$', 'delete_group', name="customers-delete_group"),
+ url(r'^(?P<group>[\w\-]+)/$', 'index', name="customers-list"),
+ url(r'^(?P<group>[\w\-]+)/upload/$', 'upload', name="customers-upload"),
+ url(r'^(?P<group>[\w\-]+)/add/$', 'edit', name="customers-create_customer"),
+ url(r'^(?P<group>[\w\-]+)/(?P<pk>\d+)/$', 'view', name="customers-view_customer"),
+ url(r'^(?P<group>[\w\-]+)/(?P<pk>\d+)/edit/$', 'edit', name="customers-edit_customer"),
+ url(r'^(?P<group>[\w\-]+)/(?P<pk>\d+)/delete/$', 'delete', name="customers-delete_customer"),
+ url(r'^(?P<pk>\d+)/move/$', 'move', name="customers-move_customer"),
+ url(r'^(?P<pk>\d+)/move/(?P<new_parent>\d+)/$', 'move', name="customers-move_customer"),
+ url(r'^(?P<pk>\d+)/merge/$', 'merge', name="customers-merge_customer"),
+ url(r'^(?P<pk>\d+)/merge/(?P<target>\d+)/$', 'merge', name="customers-merge_customer"),
+ url(r'^(?P<parent_id>\d+)/new/$', 'edit', name="customers-create_contact"),
+ url(r'^(\d+)/orders/(\d+)/$', 'add_order', name="customers-add_to_order"),
+ url(r'^(?P<pk>\d+)/notes/$', 'notes', name="customers-list_notes"),
+ url(r'^(?P<pk>\d+)/notes/new/$', 'create_message', name="customers-create_message"),
+)
diff --git a/servo/urls/default.py b/servo/urls/default.py
new file mode 100644
index 0000000..6de2bde
--- /dev/null
+++ b/servo/urls/default.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.conf.urls import patterns, include, url
+from django.views.generic import RedirectView, TemplateView
+from servo.views import account, files, gsx
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', RedirectView.as_view(url="orders/", permanent=False), name="home"),
+
+ url(r'^checkin/', include('servo.urls.checkin')),
+ url(r'^orders/', include('servo.urls.order')),
+ url(r'^repairs/', include('servo.urls.repairs')),
+ url(r'^customers/', include('servo.urls.customer')),
+ url(r'^devices/', include('servo.urls.device')),
+ url(r'^admin/', include('servo.urls.admin')),
+
+ url(r'^stats/', include('servo.urls.stats')),
+
+ url(r'^notes/', include('servo.urls.note')),
+ url(r'^sales/', include('servo.urls.sales')),
+
+ url(r'^queues/(\d+)/statuses/$', 'servo.views.queue.statuses'),
+
+ url(r'^barcode/([\w\-]+)/$', 'servo.views.note.show_barcode', name='barcodes-view'),
+ url(r'^files/(?P<pk>\d+)/view/$', files.view_file),
+ url(r'^files/(?P<path>.+)/$', files.get_file),
+
+ url(r'^login/$', account.login, name="accounts-login"),
+ url(r'^logout/$', account.logout, name="accounts-logout"),
+ #url(r'^register/$', account.register, name="accounts-register"),
+
+ url(r'^about/$', TemplateView.as_view(template_name="about.html")),
+
+ url(r'^repairs/(\d+)/parts/(\d+)/return_label/$', gsx.return_label,
+ name="parts-return_label"),
+ url(r'^repairs/([A-Z0-9]+)/details/$', gsx.repair_details, name="repairs-get_details"),
+ url(r'^returns/part/(?P<part_id>\d+)/register_return/$', gsx.register_return,
+ name='parts-register_return'),
+
+ url(r'^events/(\d+)/ack/', 'servo.views.events.acknowledge', name="events-ack_event"),
+ url(r'^tags/(\d+)/clear/', 'servo.views.tags.clear', name="tags-clear"),
+
+ (r'^api/', include('servo.urls.api')),
+ (r'^kaboom/$', 'servo.views.error.report'),
+
+ url(r'^(?P<username>[\w@\+\-\._]+)/', include('servo.urls.account')),
+
+)
diff --git a/servo/urls/device.py b/servo/urls/device.py
new file mode 100644
index 0000000..132ffbf
--- /dev/null
+++ b/servo/urls/device.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+from django.conf.urls import patterns, url
+from django.views.decorators.cache import cache_page
+
+from servo.views.order import create
+from servo.views.device import get_gsx_search_results
+
+urlpatterns = patterns(
+ "servo.views.device",
+ url(r'^$', 'index', name="devices-list"),
+
+ url(r'^search/$', 'search'),
+ url(r'^find/$', "find", name="devices-find"),
+ url(r'^add/$', "edit_device", name="devices-add"),
+
+ url(r'^(?P<pk>\d+)/diags/$', 'diagnostics', name="devices-diagnostics"),
+
+ url(r'^(?P<pk>\d+)/update_gsx_details/$', "update_gsx_details", name="devices-update_gsx_details"),
+ url(r'^(?P<pk>\d+)/orders/(?P<order_id>\d+)/queue/(?P<queue_id>\d+)/parts/$',
+ "parts", name="devices-parts"),
+
+ url(r'^search/gsx/(?P<what>\w+)/(?P<param>\w+)/(?P<query>[~\w\s,\-\(\)/\.]+)/$',
+ "search_gsx",
+ name="devices-search_gsx"),
+
+ url(r'^search/gsx/(?P<what>\w+)/(?P<param>\w+)/(?P<query>[~\w\s,\-\(\)/\.]+)/$',
+ cache_page(60*15)(get_gsx_search_results),
+ name="devices-get_gsx_search_results"),
+
+ url(r'^choose/order/(\d+)/$', 'choose', name="devices-choose"),
+ url(r'^upload/$', 'upload_devices', name="devices-upload_devices"),
+ url(r'^(?P<device_id>\d+)/orders/create/$', create, name="devices-create_order"),
+ url(r'^(?P<pk>\d+)/get_info/$', 'get_info', name="devices-get_info"),
+
+ url(r'^(?P<product_line>\w+)/$', "index", name="devices-list_devices"),
+ url(r'^(?P<product_line>\w+)/(?P<model>[\w\-]+)/$', "index",
+ name="devices-list_devices"),
+ url(r'^(?P<product_line>\w+)/(?P<model>[\w\-]+)/parts/$', "model_parts",
+ name="devices-model_parts"),
+ url(r'^(?P<product_line>\w+)/(?P<model>[\w\-]+)/(?P<pk>\d+)/$', "view_device",
+ name="devices-view_device"),
+
+ url(r'^(?P<product_line>\w+)/(?P<model>[\w-]+)/(?P<pk>\d+)/edit/$', "edit_device",
+ name="devices-edit_device"),
+ url(r'^(?P<product_line>\w+)/(?P<model>[\w-]+)/create/$', "edit_device",
+ name="devices-create_device"),
+ url(r'^(?P<product_line>\w+)/(?P<model>[\w-]+)/(?P<pk>\d+)/delete/$', "delete_device",
+ name="devices-delete_device"),
+
+ url(r'^search$', 'search', name="devices-search"),
+)
diff --git a/servo/urls/invoices.py b/servo/urls/invoices.py
new file mode 100644
index 0000000..6003bbc
--- /dev/null
+++ b/servo/urls/invoices.py
@@ -0,0 +1,10 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ "servo.views.invoices",
+ url(r'^$', "invoices", name="invoices-index"),
+ url(r'^gsx/$', 'gsx_invoices', name="invoices-gsx_invoices"),
+
+ url(r'^(?P<pk>\d+)/$', 'view_invoice', name="invoices-view_invoice"),
+ url(r'^(?P<pk>\d+)/print/$', 'print_invoice', name="invoices-print_invoice"),
+)
diff --git a/servo/urls/note.py b/servo/urls/note.py
new file mode 100644
index 0000000..7d7d830
--- /dev/null
+++ b/servo/urls/note.py
@@ -0,0 +1,29 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ "servo.views.note",
+ url(r'^$', 'list_notes', name="notes-list_notes"),
+ url(r'^search/$', 'search', name="notes-search"),
+ url(r'^find/$', 'find', name="notes-find"),
+
+ url(r'^templates/$', 'templates'),
+ url(r'^new/$', 'edit', name="notes-create"),
+ url(r'^templates/(\d+)/$', 'templates', name='notes-template'),
+ url(r'^render_template/$', 'render_template', name='notes-render_template'),
+ url(r'^to/customer/(?P<customer>\d+)/new/$', 'edit', name="notes-create_to_customer"),
+ url(r'^(?P<pk>\d+)/toggle/tag/(?P<tag_id>\d+)/$', 'toggle_tag', name="notes-toggle_tag"),
+ url(r'^(?P<pk>\d+)/toggle/(?P<flag>[a-z]+)/$', 'toggle_flag', name="notes-toggle_flag"),
+ url(r'^(?P<parent>\d+)/reply/$', 'edit', name="notes-reply"),
+ url(r'^(?P<pk>\d+)/edit/$', 'edit', name="notes-edit"),
+ url(r'^(?P<pk>\d+)/messages/$', 'list_messages', name="notes-messages"),
+ url(r'^(?P<pk>\d+)/delete/$', 'delete_note', name='notes-delete_note'),
+ url(r'^(?P<pk>\d+)/copy/$', 'copy', name='notes-copy'),
+ url(r'^to/(?P<recipient>.+)/new/$', 'edit', name="notes-create_with_recipient"),
+ url(r'^to/(?P<recipient>.+)/order/(?P<order_id>\d+)/$', 'edit',
+ name="notes-create_with_to_and_order"),
+
+ url(r'^escalations/new/$', 'create_escalation', name="notes-create_escalation"),
+
+ url(r'^(?P<kind>\w+)/$', 'list_notes', name="notes-list_notes"),
+ url(r'^(?P<kind>\w+)/(?P<pk>\d+)/view/$', 'view_note', name="notes-view_note"),
+)
diff --git a/servo/urls/order.py b/servo/urls/order.py
new file mode 100644
index 0000000..da0b3e0
--- /dev/null
+++ b/servo/urls/order.py
@@ -0,0 +1,105 @@
+from servo.views import note
+from django.conf.urls import patterns, url
+from servo.views.order import update_order
+from servo.views.invoices import create_invoice
+from servo.views.gsx import create_repair, edit_repair, import_repair
+
+
+urlpatterns = patterns(
+ "servo.views.order",
+
+ url(r'^$', 'list_orders', name='orders-index'),
+ url(r'^search/$', 'search', name="orders-search"),
+ url(r'^batch/$', 'batch_process', name="orders-batch_process"),
+ url(r'^download/$', 'download_results', name="orders-download_results"),
+
+ # Update commands
+ url(r'^(\d+)/set_([a-z]+)/(\d+)/$', update_order, name="orders-update"),
+ url(r'^(\d+)/users/(\d+)/remove/$', "remove_user", name="orders-remove_user"),
+
+ url(r'^new/$', 'create', name='orders-create'),
+ url(r'^(\d+)/$', 'edit', name='orders-edit'),
+
+ url(r'^(?P<pk>\d+)/$', 'edit', name='order-detail'),
+
+ url(r'^(\d+)/delete/$', "delete", name="orders-delete_order"),
+ url(r'^(\d+)/copy/$', "copy_order", name="orders-copy_order"),
+ url(r'^(\d+)/follow/$', 'toggle_follow', name="orders-toggle_follow"),
+ url(r'^(\d+)/unfollow/$', 'toggle_follow'),
+
+ url(r'^(\d+)/flag/$', 'toggle_flagged', name="orders-toggle_flagged"),
+ url(r'^(\d+)/events/$', 'events', name="orders-list_events"),
+ url(r'^(\d+)/repairs/(\d+)/$', 'repair', name="repairs-view_repair"),
+ url(r'^(\d+)/repairs/(\d+)/close/$', 'complete_repair',
+ name="repairs-complete_repair"),
+ url(r'^(\d+)/device/(\d+)/queue/(\d+)/parts/$', 'parts',
+ name="orders-list_parts"),
+ url(r'^(\d+)/remove_device/(\d+)/$', "remove_device",
+ name='orders-delete_device'),
+
+ url(r'^(?P<pk>\d+)/add_device/(?P<device_id>\d+)/$', "add_device",
+ name="orders-add_device"),
+ url(r'^(?P<pk>\d+)/add_device/(?P<sn>\w+)/$', "add_device",
+ name="orders-add_device"),
+
+ url(r'^(\d+)/products/$', 'products'),
+ url(r'^(\d+)/list_products/$', 'list_products', name="orders-list_products"),
+
+ url(r'^(\d+)/close/$', 'close', name='orders-close'),
+ url(r'^(\d+)/reopen/$', 'reopen_order', name='orders-reopen_order'),
+ url(r'^(\d+)/tags/(\d+)/toggle/$', 'toggle_tag', name='orders-toggle_tag'),
+ url(r'^(\d+)/tasks/(\d+)/toggle/$', 'toggle_task', name='orders-toggle_task'),
+ url(r'^(\d+)/dispatch/$', create_invoice, name='orders-dispatch'),
+ url(r'^(\d+)/products/reserve/$', 'reserve_products',
+ name="orders-reserve_products"),
+ url(r'^(\d+)/products/(\d+)/create_device/$', 'device_from_product',
+ name="orders-create_device"),
+
+ url(r'^(?P<pk>\d+)/customer/choose/', 'choose_customer',
+ name="orders-choose_customer"),
+ url(r'^(?P<pk>\d+)/customer/(?P<customer_id>\d+)/select/$', 'select_customer',
+ name="orders-select_customer"),
+ url(r'^(?P<pk>\d+)/customer/(?P<customer_id>\d+)/remove/$', 'remove_customer',
+ name="orders-remove_customer"),
+
+ url(r'^create/product/(?P<product_id>\d+)/$', 'create',
+ name="orders-create_with_product"),
+ url(r'^create/note/(?P<note_id>\d+)/$', 'create', name="orders-create_with_note"),
+ url(r'^create/device/(?P<device_id>\d+)/$', 'create',
+ name='orders-create_with_device'),
+ url(r'^create/sn/(?P<sn>\w+)?/$', 'create', name='orders-create_with_sn'),
+ url(r'^create/customer/(?P<customer_id>\d+)?/$', 'create',
+ name="orders-create_with_customer"),
+
+ url(r'^(?P<pk>\d+)/device/(?P<device_id>\d+)/accessories/$', 'accessories',
+ name='orders-accessories'),
+ url(r'^(?P<order_id>\d+)/device/(?P<device_id>\d+)/accessories/(?P<pk>\d+)/delete/$',
+ 'delete_accessory', name='orders-delete_accessory'),
+
+ url(r'^(?P<pk>\d+)/print/(?P<kind>\w+)?/$', 'put_on_paper',
+ name="orders-print_order"),
+
+ url(r'^(?P<pk>\d+)/products/(?P<item_id>\d+)/remove/$', 'remove_product',
+ name='orders-remove_product'),
+ url(r'^(?P<pk>\d+)/products/(?P<product_id>\d+)/add/$', 'add_product',
+ name="orders-add_product"),
+ url(r'^(?P<pk>\d+)/devices/(?P<device>\d+)/parts/(?P<code>[\w\-/]+)/add/$', 'add_part', name="orders-add_part"),
+ url(r'^(?P<pk>\d+)/devices/(?P<device>\d+)/history/$', 'history', name="orders-history"),
+ url(r'^(?P<pk>\d+)/products/(?P<item_id>\d+)/report/$', 'report_product',
+ name="orders-report_product"),
+ url(r'^(?P<pk>\d+)/devices/(?P<device_id>\d+)/report/$', 'report_device',
+ name="orders-report_device"),
+ url(r'^(?P<pk>\d+)/products/(?P<item_id>\d+)/edit/$', 'edit_product',
+ name="orders-edit_product"),
+ url(r'^(?P<pk>\d+)/products/(?P<item_id>\d+)/(?P<action>\w+)/$', 'products'),
+
+ url(r'^(?P<order_id>\d+)/notes/new/$', note.edit, name="orders-add_note"),
+ url(r'^(?P<order_id>\d+)/notes/(?P<pk>\d+)/$', note.edit,
+ name="orders-edit_note"),
+ url(r'^(\d+)/device/(\d+)/repairs/(\w+)/create/$', create_repair,
+ name="repairs-create_repair"),
+ url(r'^(\d+)/repairs/(\d+)/edit/$', edit_repair, name="repairs-edit_repair"),
+
+ url(r'^(\d+)/repairs/import/$', import_repair, name="repairs-import_repair"),
+
+)
diff --git a/servo/urls/products.py b/servo/urls/products.py
new file mode 100644
index 0000000..9de2575
--- /dev/null
+++ b/servo/urls/products.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ "servo.views.product",
+
+ url(r'^tags/$', "tags", name="products-tags"),
+ url(r'^all/$', "list_products", {'group': 'all'}, name="products-list_products"),
+ url(r'^download/$', "download_products", name="products-download"),
+ url(r'^upload/$', "upload_products", name="products-upload_products"),
+ url(r'^upload/parts/$', "upload_gsx_parts", name="products-upload_gsx_parts"),
+ url(r'^update_price/(\d+)/$', "update_price", name="products-update_price"),
+
+ url(r'^all/(?P<pk>\d+)/$', "view_product", {'group': 'all'}, name="products-view_product"),
+ url(r'^(?P<group>[\w\-/]*)/(?P<pk>\d+)/view/$', "view_product",
+ name="products-view_product"),
+
+ # Editing product categories
+ url(r'^categories/create/$', "edit_category", name="products-create_category"),
+ url(r'^categories/(?P<slug>[\w\-]+)/edit/$',
+ "edit_category",
+ name="products-edit_category"),
+ url(r'^categories/(?P<slug>[\w\-]+)/delete/$',
+ "delete_category",
+ name="products-delete_category"),
+ url(r'^categories/(?P<parent_slug>[\w\-]+)/create/$',
+ "edit_category",
+ name="products-create_category"),
+
+ # Editing products
+ url(r'^create/$', "edit_product", name="products-create"),
+ url(r'^(?P<group>[\w\-]+)/create/$', "edit_product", name="products-create"),
+ url(r'^(?P<group>[\w\-/]*)/(?P<pk>\d+)/edit/$', "edit_product",
+ name="products-edit_product"),
+ url(r'^(?P<group>[\w\-/]*)/(?P<pk>\d+)/delete/$', "delete_product",
+ name="products-delete_product"),
+
+ # Choosing a product for an order
+ url(r'^choose/order/(?P<order_id>\d+)/$', "choose_product", name="products-choose"),
+
+ url(r'^(?P<group>[\w\-]+)/(?P<code>[\w\-/]+)/create/$', "edit_product",
+ name="products-create"),
+ url(r'^all/(?P<code>[\w\-/]+)/view/$',
+ "view_product", {'group': 'all'},
+ name="products-view_product"),
+ url(r'^(?P<code>[\w\-/]+)/new/$',
+ "edit_product", {'group': None},
+ name="products-create"),
+
+ url(r'^code/(?P<code>[\w\-/]+)/location/(?P<location>\d+)/get_info/$',
+ "get_info",
+ name="products-get_info"),
+
+ url(r'^(?P<group>[\w\-]+)/$', "list_products", name="products-list_products"),
+)
diff --git a/servo/urls/purchases.py b/servo/urls/purchases.py
new file mode 100644
index 0000000..8bf630b
--- /dev/null
+++ b/servo/urls/purchases.py
@@ -0,0 +1,25 @@
+from django.conf.urls import patterns, url
+from servo.views.product import choose_product
+
+urlpatterns = patterns(
+ "servo.views.purchases",
+ url(r'^$', 'list_pos', name="purchases-list_pos"),
+
+ url(r'^product/(?P<product_id>\d+)/order/$', 'create_po', name='purchases-create_po'),
+ url(r'^po/create/$', 'create_po', {'order_id': None, 'product_id': None},
+ name='purchases-create_po'),
+ url(r'^po/(\d+)/edit/$', 'edit_po', name="purchases-edit_po"),
+ url(r'^po/(\d+)/view/$', 'view_po', name="purchases-view_po"),
+ url(r'^po/(\d+)/delete/$', 'delete_po', name="purchases-delete_po"),
+ url(r'^po/(\d+)/order_stock/$', 'order_stock', name="purchases-submit_stock_order"),
+ url(r'^po/(\d+)/purchases/choose/$', choose_product,
+ {'target_url': "purchases-add_to_po"},
+ name="purchases-choose_for_po"),
+ url(r'^po/order/(?P<order_id>\d+)/$', 'create_po', name="purchases-create_po"),
+ url(r'^po/(?P<pk>\d+)/purchases/(?P<product_id>\d+)/add/$', 'add_to_po',
+ name="purchases-add_to_po"),
+ url(r'^po/(?P<pk>\d+)/purchases/(?P<item_id>\d+)/delete/$', 'delete_from_po',
+ name="purchases-delete_from_po"),
+
+ url(r'^(\w+)/(\w+)/$', 'list_pos', name="purchases-browse_pos"),
+)
diff --git a/servo/urls/repairs.py b/servo/urls/repairs.py
new file mode 100644
index 0000000..5cba1c6
--- /dev/null
+++ b/servo/urls/repairs.py
@@ -0,0 +1,11 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ "servo.views.gsx",
+ url(r'^(\d+)/delete/$', 'delete_repair', name="repairs-delete_repair"),
+ url(r'^(\d+)/parts/(\d+)/remove/$', 'remove_part', name="repairs-remove_part"),
+ url(r'^(\d+)/parts/(\d+)/add/$', 'add_part', name="repairs-add_part"),
+ url(r'^(\d+)/parts/(\d+)/update_sn/$', 'update_sn', name="repairs-update_sn"),
+ url(r'^(\d+)/copy/$', 'copy_repair', name="repairs-copy_repair"),
+ url(r'^(\d+)/check_parts/$', 'check_parts_warranty', name="repairs-check_parts"),
+)
diff --git a/servo/urls/rules.py b/servo/urls/rules.py
new file mode 100644
index 0000000..5328e28
--- /dev/null
+++ b/servo/urls/rules.py
@@ -0,0 +1,10 @@
+from django.conf.urls import patterns, include, url
+
+urlpatterns = patterns(
+ 'servo.views.rules',
+ url(r'^$', 'list_rules', name='rules-list_rules'),
+ url(r'^add/$', 'edit_rule', name='rules-create'),
+ url(r'^(?P<pk>\d+)$', 'view_rule', name='rules-view_rule'),
+ url(r'^(?P<pk>\d+)/edit/$', 'edit_rule', name='rules-edit_rule'),
+ url(r'^(?P<pk>\d+)/delete/$', 'delete_rule', name='rules-delete_rule'),
+)
diff --git a/servo/urls/sales.py b/servo/urls/sales.py
new file mode 100644
index 0000000..db8a834
--- /dev/null
+++ b/servo/urls/sales.py
@@ -0,0 +1,10 @@
+from django.conf.urls import patterns, url, include
+
+urlpatterns = patterns(
+ "",
+ url(r'^products/', include('servo.urls.products')),
+ url(r'^purchases/', include('servo.urls.purchases')),
+ url(r'^shipments/', include('servo.urls.shipments')),
+ url(r'^invoices/', include('servo.urls.invoices')),
+ url(r'^search/$', 'servo.views.product.search'),
+)
diff --git a/servo/urls/search.py b/servo/urls/search.py
new file mode 100644
index 0000000..dcd7821
--- /dev/null
+++ b/servo/urls/search.py
@@ -0,0 +1,27 @@
+from django.conf.urls import patterns, url
+from django.views.decorators.cache import cache_page
+
+from servo.views.search import search_gsx
+
+urlpatterns = patterns(
+ "servo.views.search",
+ url(r'^$', "spotlight",
+ name="search-spotlight"),
+ url(r'^gsx/(?P<what>\w+)/$', "list_gsx",
+ name="search-gsx"),
+ url(r'^gsx/(?P<what>\w+)/for/(?P<q>\w+)/$', "list_gsx",
+ name="search-gsx"),
+ # /search/gsx/parts/?productName=iPod+Shuffle...
+ url(r'^gsx/(?P<what>\w+)/(?P<arg>\w+)/(?P<value>[~\w\s,\-\(\)/\.]+)/$',
+ cache_page(60*15)(search_gsx),
+ name="search-search_gsx"),
+ url(r'^gsx/(?P<what>\w+)/results/$', "view_gsx_results",
+ name="search-gsx_results"),
+ url(r'^notes/$', "list_notes"),
+ url(r'^products/$', "list_products"),
+ url(r'^orders/$', "list_orders"),
+ url(r'^customers/$', "list_customers"),
+ url(r'^devices/$', "list_devices"),
+ url(r'^gsx/$', "list_gsx"),
+ url(r'^articles/$', "list_articles"),
+)
diff --git a/servo/urls/shipments.py b/servo/urls/shipments.py
new file mode 100644
index 0000000..580354b
--- /dev/null
+++ b/servo/urls/shipments.py
@@ -0,0 +1,28 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ "servo.views.shipments",
+ url(r'^returns/list/$', "list_bulk_returns", name="shipments-list_bulk_returns"),
+ url(r'^returns/pending/$', "edit_bulk_return", name="shipments-edit_bulk_return"),
+ url(r'^returns/pending/(?P<ship_to>\d+)/$', "edit_bulk_return", name="shipments-edit_bulk_return"),
+ url(r'^returns/(?P<pk>\d+)/$', "view_bulk_return", name="shipments-view_bulk_return"),
+ url(r'^returns/(?P<pk>\d+)/packing_list/$', "view_packing_list", name="shipments-view_packing_list"),
+
+ url(r'^incoming/$', "list_incoming", name="shipments-list_incoming"),
+ url(r'^incoming/(?P<pk>\d+)/$', "view_incoming", name="shipments-view_incoming"),
+
+ url(r'^returns/(?P<pk>\d+)/verify/$', "verify", name="shipments-verify"),
+
+ url(r'^returns/$', 'list_returns', name="shipments-returns"),
+ url(r'^incoming/date/$', 'list_incoming', {'status': 'received'}),
+ url(r'^returns/(?P<pk>\d+)/parts/(?P<part_pk>\d+)/remove/$', 'remove_from_return',
+ name="shipments-remove_from_return"),
+ url(r'^returns/(?P<pk>\d+)/parts/add/$', 'add_to_return',
+ name="shipments-pick_for_return"),
+ url(r'^returns/(?P<pk>\d+)/parts/(?P<part>\d+)/$', 'add_to_return',
+ name="shipments-add_to_return"),
+ url(r'^(?P<code>[\w\-/]+)/return_label/(?P<return_order>\d+)/$', 'return_label',
+ name="shipments-return_label"),
+ url(r'^(?P<part>\d+)/update/return_type/(?P<return_type>\d{1})/', 'update_part',
+ name="shipments-update_part"),
+)
diff --git a/servo/urls/stats.py b/servo/urls/stats.py
new file mode 100644
index 0000000..df01d06
--- /dev/null
+++ b/servo/urls/stats.py
@@ -0,0 +1,13 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ 'servo.views.stats',
+ url(r'^$', 'index', name="stats-index"),
+ url(r'^sales/$', 'sales', name="stats-sales"),
+ url(r'^queues/$', 'queues', name="stats-queues"),
+ url(r'^locations/$', 'locations', name="stats-locations"),
+ url(r'^statuses/$', 'statuses', name="stats-statuses"),
+ url(r'^data/(?P<query>[\w/\-]+)/$', 'data', name="stats-data"),
+
+ url(r'^repairs/$', 'repairs', name="stats-repairs"),
+)
diff --git a/servo/validators.py b/servo/validators.py
new file mode 100644
index 0000000..74ee7f0
--- /dev/null
+++ b/servo/validators.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import os
+import re
+import phonenumbers
+from gsxws.core import validate
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext as _
+
+
+def phone_validator(val):
+ try:
+ phonenumbers.parse(val, settings.INSTALL_COUNTRY)
+ except phonenumbers.NumberParseException:
+ raise ValidationError(_('%s is not a valid phone number') % val)
+
+def apple_sn_validator(val):
+ if validate(val.upper()) not in ('serialNumber', 'alternateDeviceId',):
+ raise ValidationError(_(u'%s is not a valid serial or IMEI number') % val)
+
+def sn_validator(val):
+ if not re.match(r'^\w*$', val):
+ raise ValidationError(_('Serial numbers may only contain letters and numbers'))
+
+def file_upload_validator(val):
+ allowed = ['.pdf', '.zip', '.doc', '.jpg', '.jpeg', '.png', '.txt', '.mov', '.m4v']
+ ext = os.path.splitext(val.name)[1].lower()
+ if not ext in allowed:
+ raise ValidationError(_('Invalid file type: %s') % ext)
diff --git a/servo/views/__init__.py b/servo/views/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/views/__init__.py
diff --git a/servo/views/account.py b/servo/views/account.py
new file mode 100644
index 0000000..39193b6
--- /dev/null
+++ b/servo/views/account.py
@@ -0,0 +1,450 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import csv
+import pytz
+from datetime import date
+
+from django.contrib import auth
+from django.utils import timezone, translation
+
+from django.contrib import messages
+from django.http import HttpResponse
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect, render
+from dateutil.relativedelta import relativedelta
+from django.utils.translation import ugettext as _
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.views.order import prepare_list_view
+
+from servo.models import Order, User, Calendar, CalendarEvent
+from servo.forms.account import ProfileForm, RegistrationForm, LoginForm
+
+
+def settings(request, username):
+ """
+ User editing their profile preferences
+ """
+ title = _("Profile Settings")
+ form = ProfileForm(instance=request.user)
+
+ if request.method == "POST":
+
+ form = ProfileForm(request.POST, request.FILES, instance=request.user)
+
+ if form.is_valid():
+ user = form.save()
+ messages.success(request, _("Settings saved"))
+ User.refresh_nomail()
+
+ if form.cleaned_data['password1']:
+ request.user.set_password(form.cleaned_data['password1'])
+ request.user.save()
+
+ lang = user.activate_locale()
+ translation.activate(lang)
+ request.session[translation.LANGUAGE_SESSION_KEY] = lang
+ request.session['django_timezone'] = user.timezone
+
+ return redirect(settings, username)
+ else:
+ print("Error in user settings: %s" % form.errors)
+ messages.error(request, _("Error in user details"))
+
+ return render(request, "accounts/settings.html", locals())
+
+
+def orders(request, username):
+ """
+ This is basically like orders/index, but limited to the user
+ First, filter by the provided search criteria,
+ then check if we have a saved search filter
+ then default to user id
+ Always update saved search filter
+ """
+ args = request.GET.copy()
+
+ if not args:
+ args = request.session.get("account_search_filter", args)
+
+ if not args:
+ args.update({'state': 1}) # default to open cases
+
+ # Filter by the user, no matter what
+ args.update({'followed_by': request.user.pk})
+ request.session['account_search_filter'] = args
+
+ data = prepare_list_view(request, args)
+ data['title'] = _("My Orders")
+
+ del(data['form'].fields['assigned_to'])
+
+ return render(request, "accounts/orders.html", data)
+
+
+def login(request):
+ """
+ User trying to log in
+ """
+ title = _("Sign In")
+ form = LoginForm()
+
+ if 'username' in request.POST:
+
+ form = LoginForm(request.POST)
+
+ if form.is_valid():
+ user = auth.authenticate(
+ username=form.cleaned_data['username'],
+ password=form.cleaned_data['password']
+ )
+
+ if user is None:
+ messages.error(request, _("Incorrect username or password"))
+ elif not user.is_active:
+ messages.error(request, _("Your account has been deactivated"))
+ else:
+ auth.login(request, user)
+
+ if user.location:
+ lang = user.activate_locale()
+ request.session['django_language'] = lang
+ request.session['django_timezone'] = user.timezone
+
+ messages.success(request, _(u"%s logged in") % user.get_full_name())
+
+ if request.GET.get('next'):
+ return redirect(request.GET['next'])
+ else:
+ return redirect(orders, username=user.username)
+ else:
+ messages.error(request, _("Invalid input for login"))
+
+ return render(request, "accounts/login.html", locals())
+
+
+def logout(request):
+ if request.method == 'POST':
+ auth.logout(request)
+ messages.info(request, _("You have logged out"))
+
+ return redirect(login)
+
+ return render(request, "accounts/logout.html")
+
+
+@permission_required("servo.add_calendar")
+def calendars(request, username=None):
+ data = {'title': _('Calendars')}
+ data['calendars'] = Calendar.objects.filter(user=request.user)
+
+ if data['calendars'].count() > 0:
+ cal = data['calendars'][0]
+ return redirect(view_calendar, username, cal.pk)
+
+ return render(request, "accounts/calendars.html", data)
+
+
+@permission_required("servo.add_calendar")
+def prepare_calendar_view(request, pk, view, start_date):
+ """
+ Prepares a calendar detail view for other views to use
+ """
+ calendar = Calendar.objects.get(user=request.user, pk=pk)
+
+ if start_date is not None:
+ year, month, day = start_date.split("-")
+ start_date = date(int(year), int(month), int(day))
+ else:
+ start_date = timezone.now().date()
+
+ start = start_date
+ finish = start_date + relativedelta(days=+1)
+
+ if view == "week":
+ start = start_date + relativedelta(day=1)
+ finish = start_date + relativedelta(weeks=+1)
+
+ if view == "month":
+ start = start_date + relativedelta(day=1)
+ finish = start_date + relativedelta(day=1, months=+1, days=-1)
+
+ data = {'title': "%s %s - %s" % (calendar.title, start.strftime("%x"),
+ finish.strftime("%x"))}
+
+ data['view'] = view
+ data['start'] = start
+ data['finish'] = finish
+
+ data['next'] = finish + relativedelta(days=+1)
+ data['previous'] = start + relativedelta(days=-1)
+
+ data['calendars'] = Calendar.objects.filter(user=request.user)
+ data['events'] = calendar.calendarevent_set.filter(
+ started_at__range=(start, finish)
+ )
+
+ data['calendar'] = calendar
+ data['subtitle'] = calendar.subtitle(start, finish)
+
+ return data
+
+
+@permission_required("servo.add_calendar")
+def download_calendar(request, username, pk, view):
+ calendar = Calendar.objects.get(pk=pk)
+
+ response = HttpResponse(content_type="text/csv")
+ response['Content-Disposition'] = 'attachment; filename="%s.csv"' % calendar.title
+ writer = csv.writer(response)
+ writer.writerow(['START', 'FINISH', 'HOURS', 'NOTES'])
+
+ for e in calendar.calendarevent_set.all():
+ writer.writerow([e.started_at, e.finished_at, e.get_hours(), e.notes])
+
+ return response
+
+
+@permission_required("servo.add_calendar")
+def print_calendar(request, username, pk, view, start_date):
+ data = prepare_calendar_view(request, pk, view, start_date)
+ calendar = data['calendar']
+
+ data['location'] = request.user.location
+ # Don't show unfinished events in the report
+ data['events'] = data['events'].exclude(finished_at=None)
+ data['subtitle'] = calendar.subtitle(data['start'], data['finish'])
+ return render(request, "accounts/print_calendar.html", data)
+
+
+@permission_required("servo.add_calendar")
+def view_calendar(request, username, pk, view, start_date=None):
+ data = prepare_calendar_view(request, pk, view, start_date)
+ data['base_url'] = reverse(view_calendar, args=[username, pk, view])
+
+ return render(request, "accounts/view_calendar.html", data)
+
+
+@permission_required("servo.delete_calendar")
+def delete_calendar(request, username, pk):
+ calendar = Calendar.objects.get(pk=pk)
+
+ if calendar.user != request.user:
+ messages.error(request, _("Users can only delete their own calendars!"))
+
+ return redirect(calendars, username=username)
+
+ if request.method == "POST":
+ calendar.delete()
+ messages.success(request, _('Calendar deleted'))
+ return redirect(calendars, username=request.user.username)
+
+ data = {'title': _("Really delete this calendar?")}
+ data['action'] = request.path
+
+ return render(request, "accounts/delete_calendar.html", data)
+
+
+@permission_required("servo.change_calendar")
+def edit_calendar(request, username, pk=None, view="week"):
+ from servo.models.calendar import CalendarForm
+ calendar = Calendar(user=request.user)
+
+ if pk is not None:
+ calendar = Calendar.objects.get(pk=pk)
+
+ if request.method == "POST":
+ form = CalendarForm(request.POST, instance=calendar)
+
+ if form.is_valid():
+ calendar = form.save()
+ messages.success(request, _("Calendar saved"))
+ return redirect(view_calendar, username, calendar.pk, 'week')
+
+ form = CalendarForm(instance=calendar)
+
+ data = {'title': calendar.title}
+ data['form'] = form
+ data['action'] = request.path
+
+ return render(request, "accounts/calendar_form.html", data)
+
+
+@permission_required('servo.change_calendar')
+def edit_calendar_event(request, username, cal_pk, pk=None):
+ from servo.models.calendar import CalendarEventForm
+
+ calendar = Calendar.objects.get(pk=cal_pk)
+ event = CalendarEvent(calendar=calendar)
+
+ if pk:
+ event = CalendarEvent.objects.get(pk=pk)
+ else:
+ event.save()
+ messages.success(request, _(u'Calendar event created'))
+ return redirect(event.calendar)
+
+ form = CalendarEventForm(instance=event)
+
+ if request.method == 'POST':
+ form = CalendarEventForm(request.POST, instance=event)
+
+ if form.is_valid():
+ event = form.save()
+ messages.success(request, _(u'Event saved'))
+ return redirect(event.calendar)
+
+ data = {'title': _(u'Edit Event')}
+ data['form'] = form
+ data['calendars'] = Calendar.objects.filter(user=request.user)
+
+ return render(request, 'accounts/edit_calendar_event.html', data)
+
+
+@permission_required("servo.change_calendar")
+def finish_calendar_event(request, username, cal_pk, pk):
+ event = CalendarEvent.objects.get(pk=pk)
+ event.set_finished()
+ messages.success(request, _(u'Calendar event updated'))
+
+ return redirect(view_calendar, username, cal_pk, 'week')
+
+
+def delete_calendar_event(request, username, cal_pk, pk):
+ if username != request.user.username:
+ messages.error(request, _(u'Users can only delete their own events!'))
+
+ return redirect(calendars, username=request.user.username)
+
+ event = CalendarEvent.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ event.delete()
+ messages.success(request, _('Calendar event deleted'))
+ return redirect(event.calendar)
+
+ data = {'title': _(u'Really delete this event?')}
+ data['action'] = request.path
+ return render(request, 'accounts/delete_calendar_event.html', data)
+
+
+def register(request):
+ """
+ New user applying for access
+ """
+ form = RegistrationForm()
+ data = {'title': _("Register")}
+
+ if request.method == 'POST':
+
+ form = RegistrationForm(request.POST)
+
+ if form.is_valid():
+ user = User(is_active=False)
+ user.email = form.cleaned_data['email']
+ user.last_name = form.cleaned_data['last_name']
+ user.first_name = form.cleaned_data['first_name']
+ user.set_password(form.cleaned_data['password'])
+ user.save()
+
+ messages.success(request, _(u'Your registration is now pending approval.'))
+
+ return redirect(login)
+
+ data['form'] = form
+ return render(request, 'accounts/register.html', data)
+
+
+def clear_notifications(request, username):
+ from datetime import datetime
+ ts = [int(x) for x in request.GET.get('t').split('/')]
+ ts = datetime(*ts, tzinfo=timezone.get_current_timezone())
+ notif = request.user.notifications.filter(handled_at=None)
+ notif.filter(triggered_at__lt=ts).update(handled_at=timezone.now())
+ messages.success(request, _('All notifications cleared'))
+ return redirect(request.META['HTTP_REFERER'])
+
+
+def search(request, username):
+ """
+ User searching for something from their homepage
+ """
+ query = request.GET.get("q")
+
+ if not query or len(query) < 3:
+ messages.error(request, _('Search query is too short'))
+ return redirect('accounts-list_orders', username)
+
+ request.session['search_query'] = query
+
+ # Redirect Order ID:s to the order
+ try:
+ order = Order.objects.get(code__iexact=query)
+ return redirect(order)
+ except Order.DoesNotExist:
+ pass
+
+ kwargs = request.GET.copy()
+ kwargs.update({'followed_by': request.user.pk})
+ data = prepare_list_view(request, kwargs)
+
+ data['title'] = _("Search results")
+ orders = data['queryset']
+ data['orders'] = orders.filter(customer__fullname__icontains=query)
+
+ return render(request, "accounts/orders.html", data)
+
+
+def stats(request, username):
+ from servo.views.stats import prep_view, BasicStatsForm
+ data = prep_view(request)
+ form = BasicStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = BasicStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+ data['form'] = form
+ return render(request, "accounts/stats.html", data)
+
+
+def updates(request, username):
+ title = _('Updates')
+ kind = request.GET.get('kind', 'note_added')
+ events = request.user.notifications.filter(action=kind)
+
+ page = request.GET.get("page")
+ paginator = Paginator(events, 100)
+
+ try:
+ events = paginator.page(page)
+ except PageNotAnInteger:
+ events = paginator.page(1)
+ except EmptyPage:
+ events = paginator.page(paginator.num_pages)
+
+ return render(request, "accounts/updates.html", locals())
diff --git a/servo/views/admin.py b/servo/views/admin.py
new file mode 100644
index 0000000..91f73a9
--- /dev/null
+++ b/servo/views/admin.py
@@ -0,0 +1,778 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+
+from django.db import IntegrityError, transaction
+
+from django.core.cache import cache
+from django.contrib import messages
+from django.shortcuts import render, redirect, get_object_or_404
+
+from django.conf import settings as app_settings
+from django.utils.translation import ugettext as _
+
+from django.contrib.admin.views.decorators import staff_member_required
+from django.contrib.auth.models import Group
+
+from django.forms.models import (inlineformset_factory,
+ modelform_factory,
+ modelformset_factory,)
+
+from servo.forms.admin import *
+from servo.models.common import *
+from servo.models.repair import Checklist, ChecklistItem
+from servo.models.account import User, Group
+from servo.models.product import ShippingMethod
+
+
+def prep_list_view(model):
+ title = model._meta.verbose_name_plural
+ object_list = model.objects.all()
+ return locals()
+
+
+@staff_member_required
+def list_gsx_accounts(request):
+ object_list = GsxAccount.objects.all()
+ title = GsxAccount._meta.verbose_name_plural
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/gsx/index.html', locals())
+
+
+@staff_member_required
+def edit_gsx_account(request, pk=None):
+ object_list = GsxAccount.objects.all()
+ title = GsxAccount._meta.verbose_name_plural
+
+ if pk is None:
+ act = GsxAccount()
+ else:
+ act = GsxAccount.objects.get(pk=pk)
+
+ form = GsxAccountForm(instance=act)
+
+ if request.method == 'POST':
+ form = GsxAccountForm(request.POST, instance=act)
+ if form.is_valid():
+ try:
+ act = form.save()
+ cache.delete('gsx_session')
+ try:
+ act.test()
+ messages.success(request, _(u'%s saved') % act.title)
+ return redirect(list_gsx_accounts)
+ except gsxws.GsxError, e:
+ messages.warning(request, e)
+ except IntegrityError:
+ transaction.rollback()
+ msg = _('GSX account for this sold-to and environment already exists')
+ messages.error(request, msg)
+
+ return render(request, 'admin/gsx/form.html', locals())
+
+
+@staff_member_required
+def delete_gsx_account(request, pk=None):
+ act = GsxAccount.objects.get(pk=pk)
+ if request.method == 'POST':
+ try:
+ act.delete()
+ messages.success(request, _("GSX account deleted"))
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(list_gsx_accounts)
+
+ return render(request, 'admin/gsx/remove.html', {'action': request.path})
+
+
+@staff_member_required
+def checklists(request):
+ object_list = Checklist.objects.all()
+ title = Checklist._meta.verbose_name_plural
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/checklist/index.html', locals())
+
+
+@staff_member_required
+def edit_checklist(request, pk=None):
+ object_list = Checklist.objects.all()
+ title = Checklist._meta.verbose_name_plural
+ ChecklistItemFormset = inlineformset_factory(Checklist, ChecklistItem, exclude=[])
+
+ if pk is None:
+ checklist = Checklist()
+ else:
+ checklist = Checklist.objects.get(pk=pk)
+
+ form = ChecklistForm(instance=checklist)
+ formset = ChecklistItemFormset(instance=checklist)
+
+ if request.method == 'POST':
+ form = ChecklistForm(request.POST, instance=checklist)
+
+ if form.is_valid():
+ checklist = form.save()
+ formset = ChecklistItemFormset(request.POST, instance=checklist)
+
+ if formset.is_valid():
+ formset.save()
+ messages.success(request, _('Checklist saved'))
+ return redirect(checklist.get_admin_url())
+
+ return render(request, 'admin/checklist/form.html', locals())
+
+
+@staff_member_required
+def delete_checklist(request, pk):
+ checklist = Checklist.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ checklist.delete()
+ messages.success(request, _('Checklist deleted'))
+ return redirect(checklists)
+
+ action = str(request.path)
+ title = _('Really delete this checklist?')
+ explanation = _('This will also delete all checklist values.')
+
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def tags(request, type=None):
+ if type is None:
+ type = Tag.TYPES[0][0]
+
+ title = Checklist._meta.verbose_name_plural
+ object_list = Tag.objects.filter(type=type)
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ types = Tag.TYPES
+
+ return render(request, 'admin/tags/index.html', locals())
+
+
+@staff_member_required
+def edit_tag(request, type, pk=None):
+ if pk is None:
+ tag = Tag(type=type)
+ else:
+ tag = Tag.objects.get(pk=pk)
+
+ TagForm = modelform_factory(Tag, exclude=[])
+ form = TagForm(instance=tag)
+
+ if request.method == 'POST':
+ form = TagForm(request.POST, instance=tag)
+
+ if form.is_valid():
+ tag = form.save()
+ messages.success(request, _(u'Tag %s saved') % tag.title)
+ return redirect(edit_tag, tag.type, tag.pk)
+
+ types = Tag.TYPES
+ title = Tag._meta.verbose_name_plural
+ object_list = Tag.objects.filter(type=type)
+ return render(request, 'admin/tags/form.html', locals())
+
+
+@staff_member_required
+def delete_tag(request, pk):
+ tag = Tag.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ tag.delete()
+ messages.success(request, _('Tag deleted'))
+ return redirect(tags, type=tag.type)
+
+ title = _('Really delete this tag?')
+ action = str(request.path)
+
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def settings(request):
+ title = _('System Settings')
+ ShippingMethodFormset = modelformset_factory(ShippingMethod,
+ can_delete=True,
+ extra=0,
+ exclude=[])
+ formset = ShippingMethodFormset(queryset=ShippingMethod.objects.all())
+
+ if request.method == 'POST':
+ form = SettingsForm(request.POST, request.FILES)
+
+ if not form.is_valid():
+ messages.error(request, _('Check your settings'))
+ return render(request, 'admin/settings.html', locals())
+
+ config = form.save()
+
+ if request.POST.get('update_prices'):
+ from servo.models import Product
+ for p in Product.objects.filter(fixed_price=False):
+ p.set_stock_sales_price()
+ p.set_exchange_sales_price()
+ p.save()
+
+ # formset = ShippingMethodFormset(request.POST)
+
+ # if not formset.is_valid():
+ # messages.error(request, _('Error in shipping method settings'))
+ # return render(request, 'admin/settings.html', locals())
+
+ # formset.save()
+
+ messages.success(request, _('Settings saved'))
+ return redirect(settings)
+
+ config = Configuration.conf()
+ form = SettingsForm(initial=config)
+
+ return render(request, 'admin/settings.html', locals())
+
+
+@staff_member_required
+def statuses(request):
+ object_list = Status.objects.all()
+ title = Status._meta.verbose_name_plural
+ if object_list.count() > 0:
+ return redirect(edit_status, object_list[0].pk)
+
+ return render(request, 'admin/statuses/index.html', locals())
+
+
+@staff_member_required
+def edit_status(request, pk=None):
+ if pk is None:
+ status = Status()
+ else:
+ status = Status.objects.get(pk=pk)
+
+ header = _(u'Statuses')
+ object_list = Status.objects.all()
+ form = StatusForm(instance=status)
+ title = Status._meta.verbose_name_plural
+
+ if request.method == 'POST':
+ form = StatusForm(request.POST, instance=status)
+ if form.is_valid():
+ status = form.save()
+ messages.success(request, _(u'%s saved') % status.title)
+ return redirect(edit_status, status.pk)
+
+ return render(request, 'admin/statuses/form.html', locals())
+
+
+@staff_member_required
+def remove_status(request, pk):
+ status = Status.objects.get(pk=pk)
+ action = request.path
+
+ if request.method == 'POST':
+ status.delete()
+ messages.success(request, _(u'%s deleted') % status.title)
+ return redirect(statuses)
+
+ return render(request, 'admin/statuses/remove.html', locals())
+
+
+@staff_member_required
+def fields(request, type='customer'):
+ data = prep_list_view(Property)
+ data['type'] = type
+ data['types'] = Property.TYPES
+ data['object_list'] = Property.objects.filter(type=type)
+
+ if data['object_list'].count() > 0:
+ field = data['object_list'][0]
+ return redirect(edit_field, field.type, field.pk)
+
+ return render(request, 'admin/fields/index.html', data)
+
+
+@staff_member_required
+def edit_field(request, type, pk=None):
+ if pk is None:
+ field = Property(type=type)
+ else:
+ field = Property.objects.get(pk=pk)
+
+ FieldForm = modelform_factory(Property, exclude=[])
+
+ types = Property.TYPES
+ title = Property._meta.verbose_name_plural
+ object_list = Property.objects.filter(type=type)
+ form = FieldForm(instance=field)
+
+ if request.method == 'POST':
+ form = FieldForm(request.POST, instance=field)
+
+ if form.is_valid():
+ field = form.save()
+ messages.success(request, _(u'Field saved'))
+ return redirect(field.get_admin_url())
+
+ return render(request, 'admin/fields/form.html', locals())
+
+
+@staff_member_required
+def delete_field(request, pk=None):
+ field = Property.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ field.delete()
+ messages.success(request, _(u'Field deleted'))
+ return redirect(fields, type=field.type)
+
+ data = {'title': _('Really delete this field?')}
+ data['action'] = request.path
+
+ return render(request, 'generic/delete.html', data)
+
+
+@staff_member_required
+def list_templates(request):
+ object_list = Template.objects.all()
+ title = Template._meta.verbose_name_plural
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+ return render(request, "admin/templates/list_templates.html", locals())
+
+
+@staff_member_required
+def edit_template(request, pk=None):
+
+ if pk is None:
+ template = Template()
+ else:
+ template = Template.objects.get(pk=pk)
+
+ form = TemplateForm(instance=template)
+
+ if request.method == 'POST':
+ form = TemplateForm(request.POST, instance=template)
+
+ if form.is_valid():
+ template = form.save()
+ messages.success(request, _(u'Template %s saved') % template.title)
+ # generic view...
+ return redirect(template.get_admin_url())
+
+ form = form
+ object_list = Template.objects.all()
+ title = Template._meta.verbose_name_plural
+ return render(request, 'admin/templates/form.html', locals())
+
+
+@staff_member_required
+def delete_template(request, pk):
+ template = Template.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ template.delete()
+ messages.success(request, _(u'Template %s deleted') % template.title)
+ return redirect(list_templates)
+
+ title = _('Really delete this template?')
+ action = str(request.path)
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def list_users(request):
+ object_list = User.objects.filter(is_visible=True)
+ title = User._meta.verbose_name_plural
+ locations = Location.objects.all()
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/users/index.html', locals())
+
+
+@staff_member_required
+def list_groups(request):
+ object_list = Group.objects.all()
+ title = _('Users & Groups')
+
+ return render(request, 'admin/users/groups.html', locals())
+
+
+@staff_member_required
+def edit_group(request, pk=None):
+ title = _(u'Edit Group')
+ object_list = Group.objects.all()
+
+ if pk is None:
+ group = Group()
+ form = GroupForm(instance=group)
+ else:
+ group = Group.objects.get(pk=pk)
+ title = group.name
+ form = GroupForm(instance=group)
+
+ if request.method == 'POST':
+ form = GroupForm(request.POST, instance=group)
+ if form.is_valid():
+ form.save()
+ messages.success(request, _(u'Group saved'))
+ return redirect(list_groups)
+
+ return render(request, 'admin/users/group_form.html', locals())
+
+
+@staff_member_required
+def delete_group(request, pk):
+ group = Group.objects.get(pk=pk)
+
+ if request.method == "POST":
+ group.delete()
+ messages.success(request, _("Group deleted"))
+ return redirect(list_groups)
+
+ data = {'action': request.path}
+
+ return render(request, "admin/users/delete_group.html", data)
+
+
+@staff_member_required
+def delete_user(request, user_id):
+ user = User.objects.get(pk=user_id)
+
+ if request.method == "POST":
+ try:
+ user.delete()
+ messages.success(request, _("User deleted"))
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(list_users)
+
+ return render(request, "admin/users/remove.html", locals())
+
+
+@staff_member_required
+def delete_user_token(request, user_id):
+ user = User.objects.get(pk=user_id)
+ user.delete_tokens()
+ messages.success(request, _('API tokens deleted'))
+ return redirect(edit_user, user.pk)
+
+
+@staff_member_required
+def create_user_token(request, user_id):
+ user = User.objects.get(pk=user_id)
+ token = user.create_token()
+ messages.success(request, _('API token created'))
+ return redirect(edit_user, user.pk)
+
+
+@staff_member_required
+def edit_user(request, pk=None):
+ if pk is None:
+ user = User(site_id=app_settings.SITE_ID)
+ user.location = request.user.location
+ user.locale = request.user.locale
+ user.region = request.user.region
+ user.timezone = request.user.timezone
+ else:
+ user = User.objects.get(pk=pk)
+
+ form = UserForm(instance=user)
+
+ if request.method == "POST":
+ form = UserForm(request.POST, instance=user)
+ if form.is_valid():
+ user = form.save()
+ User.refresh_nomail()
+ if request.POST.get('password1'):
+ user.set_password(request.POST['password1'])
+ user.save()
+ messages.success(request, _(u"User %s saved") % user.get_name())
+ return redirect(edit_user, user.pk)
+ else:
+ messages.error(request, _("Error in user profile data"))
+
+ object_list = User.objects.filter(is_visible=True)
+
+ if request.GET.get('l'):
+ object_list = object_list.filter(locations__pk=request.GET['l'])
+
+ title = User._meta.verbose_name_plural
+ locations = Location.objects.all()
+
+ if len(object_list) > 0:
+ header = _(u'%d users') % len(object_list)
+
+ return render(request, "admin/users/form.html", locals())
+
+
+@staff_member_required
+def locations(request):
+ object_list = Location.objects.all()
+ title = Location._meta.verbose_name_plural
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/locations/index.html', locals())
+
+
+@staff_member_required
+def edit_location(request, pk=None):
+ header = _('Locations')
+ object_list = Location.objects.all()
+ title = Location._meta.verbose_name_plural
+
+ if pk is None:
+ location = Location()
+ location.timezone = request.user.timezone
+ else:
+ location = Location.objects.get(pk=pk)
+
+ form = LocationForm(instance=location)
+
+ if request.method == 'POST':
+ form = LocationForm(request.POST, request.FILES, instance=location)
+ if form.is_valid():
+ try:
+ location = form.save()
+ messages.success(request, _(u'Location %s saved') % location.title)
+ return redirect(location.get_admin_url())
+ except Exception:
+ pass # just show the form with the error
+
+ return render(request, 'admin/locations/form.html', locals())
+
+
+@staff_member_required
+def delete_location(request, pk):
+ location = Location.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ location.delete()
+ messages.success(request, _(u'%s deleted') % location.title)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(locations)
+
+ title = _(u'Really delete this location?')
+ explanation = _(u'This will not delete the orders at this location')
+ action = request.path
+
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def queues(request):
+ data = prep_list_view(Queue)
+ if data['object_list'].count() > 0:
+ return redirect(data['object_list'][0].get_admin_url())
+ data['subtitle'] = _('Create, edit and delete service queues')
+ return render(request, 'admin/queues/index.html', data)
+
+
+@staff_member_required
+def edit_queue(request, pk=None):
+
+ StatusFormSet = inlineformset_factory(Queue, QueueStatus, extra=1, exclude=[])
+
+ if pk is None:
+ queue = Queue()
+ locations = request.user.locations.all()
+ form = QueueForm(initial={'locations': locations})
+ else:
+ queue = Queue.objects.get(pk=pk)
+ form = QueueForm(instance=queue, initial={'users': queue.user_set.all()})
+
+ title = _(u'Queues')
+ object_list = Queue.objects.all()
+ formset = StatusFormSet(instance=queue)
+
+ if request.method == 'POST':
+ form = QueueForm(request.POST, request.FILES, instance=queue)
+
+ if form.is_valid():
+ try:
+ queue = form.save()
+ queue.user_set = form.cleaned_data['users']
+ queue.save()
+ except Exception as e:
+ messages.error(request, _('Failed to save queue'))
+ return render(request, 'admin/queues/form.html', locals())
+
+ formset = StatusFormSet(request.POST, instance=queue)
+
+ if formset.is_valid():
+ formset.save()
+ messages.success(request, _(u'%s queue saved') % queue.title)
+ return redirect(queue.get_admin_url())
+ else:
+ messages.error(request, formset.errors)
+ else:
+ messages.error(request, form.errors)
+
+ return render(request, 'admin/queues/form.html', locals())
+
+
+@staff_member_required
+def delete_queue(request, pk=None):
+ queue = Queue.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ queue.delete()
+ messages.success(request, _("Queue deleted"))
+ except Queue.ProtectedError:
+ messages.error(request, _("Cannot delete queue"))
+
+ return redirect(queues)
+
+ return render(request, 'admin/queues/remove.html', locals())
+
+
+@staff_member_required
+def notifications(request):
+ data = {'title': _(u'Notifications')}
+ return render(request, 'admin/notifications/index.html', data)
+
+
+@staff_member_required
+def edit_notification(request, nid):
+ return render(request, 'admin/notifications/form.html')
+
+
+def list_sites(request):
+ if not request.user.is_superuser:
+ messages.error(request, _(u"Access denied"))
+ return redirect('/login/')
+
+ data = {'sites': Site.objects.all()}
+ data['title'] = _(u"Manage Sites")
+
+ return render(request, "admin/sites/index.html", data)
+
+
+def edit_site(request, pk=None):
+ if not request.user.is_superuser:
+ messages.add_message(request, messages.ERROR, _(u"Access denied"))
+ return redirect('/login/')
+
+ site = Site()
+ data = {'title': _(u"New Site")}
+
+ if pk is not None:
+ site = Site.objects.get(pk=pk)
+ data['title'] = site.name
+
+ SiteForm = modelform_factory(Site, exclude=[])
+ form = SiteForm(instance=site)
+
+ if request.method == "POST":
+
+ form = SiteForm(request.POST, instance=site)
+
+ if form.is_valid():
+ form.save()
+ messages.add_message(request, messages.SUCCESS, _(u"Site saved"))
+ return redirect(list_sites)
+
+ data['form'] = form
+ data['sites'] = Site.objects.all()
+
+ return render(request, "admin/sites/edit_site.html", data)
+
+
+def upload_users(request):
+ """
+ """
+ action = request.path
+ form = UserUploadForm()
+ title = _('Upload Users')
+
+ if request.method == 'POST':
+ form = UserUploadForm(request.POST, request.FILES)
+ if form.is_valid():
+ try:
+ users = form.save()
+ messages.success(request, _('%d users imported') % len(users))
+ except Exception, e:
+ messages.error(request, e)
+ else:
+ messages.error(request, form.errors)
+
+ return redirect(list_users)
+
+ return render(request, "admin/users/upload_users.html", locals())
+
+
+class Backup(object):
+ @classmethod
+ def all(cls):
+ from glob import glob
+ return [cls(s) for s in glob("backups/*.gz")]
+
+ def __init__(self, path):
+ import os
+ self.path = path
+ self.filename = os.path.basename(path)
+ self.filesize = os.path.getsize(path)
+
+ def get_wrapper(self):
+ from django.core.servers.basehttp import FileWrapper
+ return FileWrapper(file(self.path))
+
+ def get_response(self):
+ from django.http import HttpResponse
+ wrapper = self.get_wrapper()
+ response = HttpResponse(wrapper, content_type='application/force-download')
+ response['Content-Disposition'] = 'attachment; filename=%s' % self.filename
+ response['Content-Length'] = self.filesize
+ return response
+
+def backups(request):
+
+ if request.GET.get('dl'):
+ backup = Backup("backups/%s" % request.GET['dl'])
+ return backup.get_response()
+
+ title = _('Backups')
+ backups = Backup.all()
+ return render(request, "admin/backups.html", locals())
diff --git a/servo/views/api.py b/servo/views/api.py
new file mode 100644
index 0000000..87234f3
--- /dev/null
+++ b/servo/views/api.py
@@ -0,0 +1,401 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+from django.utils import timezone
+from django.http import HttpResponse
+from django.core.exceptions import FieldError
+from django.core.serializers import serialize
+from django.shortcuts import get_object_or_404
+from django.views.generic.detail import DetailView
+
+from rest_framework.response import Response
+
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.authentication import TokenAuthentication, SessionAuthentication
+from rest_framework.decorators import (api_view, authentication_classes, permission_classes)
+
+from servo.api.serializers import *
+
+from servo.models import *
+
+
+def dumps(obj):
+ import datetime
+ data = {}
+ for f in obj.api_fields:
+ value = getattr(obj, f)
+ if type(value) in (datetime.datetime, datetime.date,):
+ value = value.isoformat()
+ data[f] = value
+ return json.dumps(data)
+
+
+class OrderStatusView(DetailView):
+
+ model = Order
+
+ def get(self, *args):
+ args = self.request.GET
+ if not args.get('q'):
+ error = {'error': 'Need parameter for query'}
+ return HttpResponse(json.dumps(error),
+ status=400,
+ content_type='application/json')
+
+ self.code = args.get('q')
+ self.object = get_object_or_404(Order, code=self.code)
+ context = self.get_context_data(object=self.object)
+ return self.render_to_response(context)
+
+ def render_to_response(self, context, **response_kwargs):
+ out = {
+ 'order': self.object.code,
+ 'status': self.object.get_status_name(),
+ 'status_description': self.object.get_status_description(),
+ }
+
+ if Configuration.conf('checkin_timeline'):
+ timeline = []
+ for i in self.object.orderstatus_set.exclude(status=None):
+ status = {'badge': i.get_badge()}
+ status['status'] = i.status.title
+ status['started_at'] = i.started_at.isoformat()
+ status['description'] = i.status.description
+ timeline.append(status)
+
+ out['timeline'] = timeline
+
+ return HttpResponse(json.dumps(out), content_type='application/json')
+
+
+def tags(request):
+ results = Tag.objects.filter(**request.GET.dict())
+ data = results.distinct().values_list("title", flat=True)
+ return HttpResponse(json.dumps(list(data)), content_type='application/json')
+
+
+def statuses(request):
+ from servo.models import Status
+ results = Status.objects.all()
+ data = serialize('json', results)
+ return HttpResponse(data, content_type='application/json')
+
+
+def locations(request):
+ queryset = Location.objects.all()
+ serializer = 'json'
+ if request.META['HTTP_USER_AGENT'].startswith('curl'):
+ serializer = 'yaml'
+ data = serialize(serializer, queryset)
+ return HttpResponse(data)
+
+
+@api_view(['GET'])
+@authentication_classes((SessionAuthentication, TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def users(request):
+ query = request.GET.dict()
+ queryset = User.active.filter(**query)
+ data = list(queryset.values_list("full_name", flat=True))
+ return HttpResponse(json.dumps(data), content_type='application/json')
+
+
+def places(request):
+ places = Order.objects.exclude(place=None)
+ places = places.order_by("place").distinct("place").values_list('place', flat=True)
+ return HttpResponse(json.dumps(list(places)), content_type='application/json')
+
+
+def queues(request):
+ queryset = Queue.objects.all()
+ data = serialize('json', queryset, fields=('pk', 'title'))
+ return HttpResponse(data, content_type='application/json')
+
+
+def json_response(data):
+ return HttpResponse(json.dumps(data), content_type='application/json')
+
+
+def ok(message):
+ msg = json.dumps(dict(ok=message))
+ return HttpResponse(msg, content_type='application/json')
+
+
+def error(message):
+ msg = json.dumps(dict(error=str(message)))
+ return HttpResponse(msg, content_type='application/json')
+
+
+def client_error(message):
+ msg = json.dumps(dict(error=str(message)))
+ return HttpResponse(msg, content_type='application/json', status=400)
+
+
+def create_order(request):
+ try:
+ data = json.loads(request.body)
+ except ValueError as e:
+ return client_error('Malformed request: %s' % e)
+
+ cdata = data.get('customer')
+ problem = data.get('problem')
+
+ if not cdata:
+ return client_error('Cannot create order without customer info')
+
+ if not problem:
+ return client_error('Cannot create order without problem description')
+
+ try:
+ customer, created = Customer.objects.get_or_create(
+ name=cdata['name'],
+ email=cdata['email']
+ )
+ except Exception as e:
+ return client_error('Invalid customer details: %s' % e)
+
+ if request.user.customer:
+ customer.parent = request.user.customer
+
+ if cdata.get('city'):
+ customer.city = cdata.get('city')
+
+ if cdata.get('phone'):
+ customer.phone = cdata.get('phone')
+
+ if cdata.get('zip_code'):
+ customer.zip_code = cdata.get('zip_code')
+
+ if cdata.get('street_address'):
+ customer.street_address = cdata.get('street_address')
+
+ customer.save()
+
+ order = Order(created_by=request.user, customer=customer)
+ order.save()
+
+ note = Note(created_by=request.user, body=problem, is_reported=True)
+ note.order = order
+ note.save()
+
+ if data.get('attachment'):
+ import base64
+ from servo.models import Attachment
+ from django.core.files.base import ContentFile
+
+ attachment = data.get('attachment')
+
+ try:
+ filename = attachment.get('name')
+ content = base64.b64decode(attachment.get('data'))
+ except Exception as e:
+ return client_error('Invalid file data: %s' %e)
+
+ content = ContentFile(content, filename)
+ attachment = Attachment(content=content, content_object=note)
+ attachment.save()
+ attachment.content.save(filename, content)
+ note.attachments.add(attachment)
+
+ if data.get('device'):
+
+ try:
+ GsxAccount.default(request.user)
+ except Exception as e:
+ pass
+
+ ddata = data.get('device')
+
+ try:
+ device = order.add_device_sn(ddata.get('sn'), request.user)
+ except Exception as e:
+ device = Device(sn=ddata.get('sn', ''))
+ device.description = ddata.get('description', '')
+ device.save()
+ order.add_device(device)
+
+ for a in ddata.get('accessories', []):
+ a = Accessory(name=a, order=order, device=device)
+ a.save()
+
+ return ok(order.code)
+
+
+@api_view(['GET', 'POST', 'PUT'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def orders(request, code=None, pk=None):
+ """
+ This is the orders API
+ """
+ from servo.api.serializers import OrderSerializer
+
+ if request.method == 'POST':
+ return create_order(request)
+
+ if request.method == 'PUT':
+ return error('Method not yet implemented')
+
+ if request.GET.get('q'):
+ results = Order.objects.filter(**request.GET)
+
+ if pk:
+ order = Order.objects.get(pk=pk)
+ serializer = OrderSerializer(order, context={'request': request})
+ return Response(serializer.data)
+
+ if code:
+ order = Order.objects.get(code=code)
+ if order.status:
+ order.status_description = order.status.status.description
+ serializer = OrderSerializer(order, context={'request': request})
+ return Response(serializer.data)
+
+ orders = Order.objects.none()
+ serializer = OrderSerializer(orders, many=True, context={'request': request})
+ return Response(serializer.data)
+
+
+def messages(request):
+ """
+ Responds to SMS status updates
+ """
+ from servo.messaging.sms import SMSJazzProvider, HQSMSProvider
+
+ if not request.GET.get('id'):
+ return HttpResponse('Thanks, but no thanks')
+
+ m = get_object_or_404(Message, code=request.GET['id'])
+ gw = Configuration.conf('sms_gateway')
+ statusmap = HQSMSProvider.STATUSES
+
+ if gw == 'jazz':
+ statusmap = SMSJazzProvider.STATUSES
+
+ status = statusmap[request.GET['status']]
+ m.status = status[0]
+ m.error = status[1]
+
+ if m.status == 'DELIVERED':
+ m.received_at = timezone.now()
+
+ if m.status == 'FAILED':
+ if m.note.order:
+ uid = Configuration.conf('imap_act')
+ if uid:
+ user = User.objects.get(pk=uid)
+ m.note.order.notify('sms_failed', m.error, user)
+
+ m.save()
+
+ return HttpResponse('OK')
+
+
+def device_models(request):
+ data = Device.objects.order_by("description").distinct("description")
+ return json_response(list(data.values_list("description", flat=True)))
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def warranty(request):
+ from servo.api.serializers import DeviceSerializer
+ sn = request.GET.get('sn')
+
+ if not sn:
+ return error('Need query parameter for warranty lookup')
+
+ try:
+ GsxAccount.default(request.user)
+ except Exception as e:
+ return error('Cannot connect to GSX (check user name and password)')
+
+ try:
+ result = Device.from_gsx(sn, cached=False)
+ serializer = DeviceSerializer(result, context={'request': request})
+ return Response(serializer.data)
+ except Exception as e:
+ return error(e)
+
+
+@api_view(['GET'])
+def order_status(request):
+ from servo.api.serializers import OrderStatusSerializer
+ code = request.GET.get('q')
+ try:
+ result = Order.objects.get(code=code)
+ #serializer = OrderStatusSerializer(result)
+ return Response(serializer.data)
+ except Exception as e:
+ return (error(e))
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def notes(request, pk=None):
+ if pk:
+ note = Note.objects.get(pk=pk)
+ serializer = NoteSerializer(note, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def order_items(request, pk):
+ item = ServiceOrderItem.objects.get(pk=pk)
+ serializer = ServiceOrderItemSerializer(item, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def user_detail(request, pk):
+ user = User.objects.get(pk=pk)
+ serializer = UserSerializer(user, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def customers(request, pk=None):
+ customer = Customer.objects.get(pk=pk)
+ serializer = CustomerSerializer(customer, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def devices(request, pk=None):
+ device = Device.objects.get(pk=pk)
+ serializer = DeviceSerializer(device, context={'request': request})
+ return Response(serializer.data)
diff --git a/servo/views/checkin.py b/servo/views/checkin.py
new file mode 100644
index 0000000..7b6787a
--- /dev/null
+++ b/servo/views/checkin.py
@@ -0,0 +1,418 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+import locale
+
+from gsxws import products, GsxError
+
+from django.conf import settings
+from django.http import HttpResponse
+from django.contrib import messages
+from django.core.cache import cache
+
+from django.utils import translation
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext as _
+from django.shortcuts import render, redirect, get_object_or_404
+
+from servo.views.order import put_on_paper
+from servo.validators import apple_sn_validator
+from servo.models import (User, Device, GsxAccount, Order,
+ Customer, Location, Note, Attachment,
+ Configuration, ChecklistItem, Tag,)
+from servo.forms import (SerialNumberForm, AppleSerialNumberForm,
+ DeviceForm, IssueForm, CustomerForm,
+ QuestionForm, AttachmentForm, StatusCheckForm,)
+
+
+def init_locale(request):
+ lc = settings.INSTALL_LOCALE.split('.')
+ locale.setlocale(locale.LC_TIME, lc)
+ locale.setlocale(locale.LC_NUMERIC, lc)
+ locale.setlocale(locale.LC_MESSAGES, lc)
+ locale.setlocale(locale.LC_MONETARY, lc)
+
+ translation.activate(settings.INSTALL_LANGUAGE)
+ request.session[translation.LANGUAGE_SESSION_KEY] = settings.INSTALL_LANGUAGE
+
+
+def set_cache_device(device):
+ key = 'checkin-device-%s' % device.sn
+ cache.set(key, device)
+
+
+def get_gsx_connection(request):
+ act = GsxAccount.get_default_account()
+ user = User.objects.get(pk=request.session['checkin_user'])
+ location = Location.objects.get(pk=request.session['checkin_location'])
+ return act.connect(user, location)
+
+
+def get_remote_device(request, sn):
+ try:
+ apple_sn_validator(sn)
+ except ValidationError:
+ return Device(sn=sn, image_url='https://static.servoapp.com/images/na.gif')
+
+ get_gsx_connection(request)
+
+ return Device.from_gsx(sn)
+
+
+def get_local_device(request, sn):
+ try:
+ device = Device.objects.filter(sn=sn)[0]
+ except IndexError:
+ device = get_remote_device(request, sn)
+
+ return device
+
+
+def get_device(request, sn):
+ if len(sn) < 1:
+ return Device(sn=sn)
+
+ key = 'checkin-device-%s' % sn
+ device = cache.get(key, get_local_device(request, sn))
+ set_cache_device(device)
+ return device
+
+
+def reset_session(request):
+
+ # initialize some basic vars
+ if not request.user.is_authenticated():
+ request.session.flush()
+
+ # initialize locale
+ init_locale(request)
+
+ request.session['checkin_device'] = None
+ request.session['checkin_customer'] = None
+
+ if not request.session.get('company_name'):
+ request.session['company_name'] = Configuration.conf('company_name')
+
+ if request.user.is_authenticated():
+
+ if request.GET.get('u'):
+ user = User.objects.get(pk=request.GET['u'])
+ else:
+ user = request.user
+
+ if request.GET.get('l'):
+ location = Location.objects.get(pk=request.GET['l'])
+ else:
+ location = user.location
+
+ checkin_users = User.get_checkin_group()
+ request.session['checkin_users'] = User.get_checkin_group_list()
+ request.session['checkin_locations'] = request.user.get_location_list()
+
+ queryset = checkin_users.filter(location=location)
+ request.session['checkin_users'] = User.serialize(queryset)
+
+ else:
+ user = User.get_checkin_user()
+ location = user.location
+
+ request.session['checkin_user'] = user.pk
+ request.session['checkin_location'] = location.pk
+ request.session['checkin_user_name'] = user.get_name()
+ request.session['checkin_location_name'] = location.title
+
+
+def reset(request):
+ reset_session(request)
+ return redirect(index)
+
+
+def thanks(request, order):
+ """
+ Final step/confirmation
+ """
+ title = _('Done!')
+
+ try:
+ request.session.delete_test_cookie()
+ except KeyError:
+ pass # ignore spurious KeyError at /checkin/thanks/RJTPS/
+
+ try:
+ order = Order.objects.get(url_code=order)
+ except Order.DoesNotExist:
+ messages.error(request, _('Order does not exist'))
+ return redirect(reset)
+
+ return render(request, "checkin/thanks.html", locals())
+
+
+def get_customer(request):
+ if not request.user.is_authenticated():
+ return
+
+ if not request.GET.get('c'):
+ return
+
+ customer = Customer.objects.get(pk=request.GET['c'])
+ request.session['checkin_customer'] = customer.pk
+
+ fdata = {'fname': customer.firstname}
+ fdata['lname'] = customer.lastname
+ fdata['email'] = customer.email
+ fdata['city'] = customer.city
+ fdata['phone'] = customer.phone
+ fdata['country'] = customer.country
+ fdata['address'] = customer.street_address
+ fdata['postal_code'] = customer.zip_code
+
+ return HttpResponse(json.dumps(fdata), content_type='application/json')
+
+
+def status(request):
+ """
+ Status checking through the checkin
+ """
+ title = _('Repair Status')
+
+ if request.GET.get('code'):
+ timeline = []
+ form = StatusCheckForm(request.GET)
+ if form.is_valid():
+ code = form.cleaned_data['code']
+ try:
+ order = Order.objects.get(code=code)
+ if Configuration.conf('checkin_timeline'):
+ timeline = order.orderstatus_set.all()
+ if order.status is None:
+ order.status_name = _(u'Waiting to be processed')
+ except Order.DoesNotExist:
+ messages.error(request, _(u'Order %s not found') % code)
+ return render(request, "checkin/status-show.html", locals())
+ else:
+ form = StatusCheckForm()
+
+ return render(request, "checkin/status.html", locals())
+
+
+def print_confirmation(request, code):
+ order = Order.objects.get(url_code=code)
+ return put_on_paper(request, order.pk)
+
+
+def terms(request):
+ conf = Configuration.conf()
+ return render(request, 'checkin/terms.html', locals())
+
+
+def index(request):
+
+ if request.method == 'GET':
+ reset_session(request)
+
+ title = _('Service Order Check-In')
+
+ dcat = request.GET.get('d', 'mac')
+ dmap = {
+ 'mac' : _('Mac'),
+ 'iphone' : _('iPhone'),
+ 'ipad' : _('iPad'),
+ 'ipod' : _('iPod'),
+ 'acc' : _('Apple Accessory'),
+ 'beats' : _('Beats Products'),
+ 'other' : _('Other Devices'),
+ }
+
+ issue_form = IssueForm()
+ device = Device(description=dmap[dcat])
+
+ if dcat in ('mac', 'iphone', 'ipad', 'ipod'):
+ sn_form = AppleSerialNumberForm()
+ else:
+ sn_form = SerialNumberForm()
+
+ tags = Tag.objects.filter(type="order")
+ device_form = DeviceForm(instance=device)
+ customer_form = CustomerForm(request)
+
+ if request.method == 'POST':
+
+ sn_form = SerialNumberForm(request.POST)
+ issue_form = IssueForm(request.POST, request.FILES)
+ customer_form = CustomerForm(request, request.POST)
+ device_form = DeviceForm(request.POST, request.FILES)
+
+ if device_form.is_valid() and issue_form.is_valid() and customer_form.is_valid():
+
+ user = User.objects.get(pk=request.session['checkin_user'])
+
+ idata = issue_form.cleaned_data
+ ddata = device_form.cleaned_data
+ cdata = customer_form.cleaned_data
+
+ customer_id = request.session.get('checkin_customer')
+ if customer_id:
+ customer = Customer.objects.get(pk=customer_id)
+ else:
+ customer = Customer()
+
+ name = u'{0} {1}'.format(cdata['fname'], cdata['lname'])
+
+ if len(cdata['company']):
+ name += ', ' + cdata['company']
+
+ customer.name = name
+ customer.city = cdata['city']
+ customer.phone = cdata['phone']
+ customer.email = cdata['email']
+ customer.phone = cdata['phone']
+ customer.zip_code = cdata['postal_code']
+ customer.street_address = cdata['address']
+ customer.save()
+
+ order = Order(customer=customer, created_by=user)
+ order.location_id = request.session['checkin_location']
+ order.checkin_location = cdata['checkin_location']
+ order.checkout_location = cdata['checkout_location']
+
+ order.save()
+ order.check_in(user)
+
+ try:
+ device = get_device(request, ddata['sn'])
+ except GsxError as e:
+ pass
+
+ device.username = ddata['username']
+ device.password = ddata['password']
+ device.description = ddata['description']
+ device.purchased_on = ddata['purchased_on']
+ device.purchase_country = ddata['purchase_country']
+ device.save()
+
+ order.add_device(device, user)
+
+ note = Note(created_by=user, body=idata['issue_description'])
+ note.is_reported = True
+ note.order = order
+ note.save()
+
+ # Proof of purchase was supplied
+ if ddata.get('pop'):
+ f = {'content_type': Attachment.get_content_type('note').pk}
+ f['object_id'] = note.pk
+ a = AttachmentForm(f, {'content': ddata['pop']})
+ a.save()
+
+ if request.POST.get('tags'):
+ order.set_tags(request.POST.getlist('tags'), request.user)
+
+ # Check checklists early for validation
+ answers = []
+
+ # @FIXME: should try to move this to a formset...
+ for k, v in request.POST.items():
+ if k.startswith('__cl__'):
+ answers.append('- **' + k[6:] + '**: ' + v)
+
+ if len(answers) > 0:
+ note = Note(created_by=user, body="\r\n".join(answers))
+
+ if Configuration.true('checkin_report_checklist'):
+ note.is_reported = True
+
+ note.order = order
+ note.save()
+
+ # mark down internal notes (only if logged in)
+ if len(idata.get('notes')):
+ note = Note(created_by=user, body=idata['notes'])
+ note.is_reported = False
+ note.order = order
+ note.save()
+
+ # mark down condition of device
+ if len(ddata.get('condition')):
+ note = Note(created_by=user, body=ddata['condition'])
+ note.is_reported = True
+ note.order = order
+ note.save()
+
+ # mark down supplied accessories
+ if len(ddata.get('accessories')):
+ accs = ddata['accessories'].strip().split("\n")
+ order.set_accessories(accs, device)
+
+ redirect_to = thanks
+
+ """
+ if request.user.is_authenticated():
+ if request.user.autoprint:
+ redirect_to = print_confirmation
+ """
+ return redirect(redirect_to, order.url_code)
+
+ try:
+ pk = Configuration.conf('checkin_checklist')
+ questions = ChecklistItem.objects.filter(checklist_id=pk)
+ except ValueError:
+ # Checklists probably not configured
+ pass
+
+ if request.GET.get('phone'):
+
+ if not request.user.is_authenticated():
+ return
+
+ results = []
+
+ for c in Customer.objects.filter(phone=request.GET['phone']):
+ title = '%s - %s' % (c.phone, c.name)
+ results.append({'id': c.pk, 'name': c.name, 'title': title})
+
+ return HttpResponse(json.dumps(results), content_type='application/json')
+
+ if request.GET.get('sn'):
+
+ device = Device(sn=request.GET['sn'])
+ device.description = _('Other Device')
+ device_form = DeviceForm(instance=device)
+
+ try:
+ apple_sn_validator(device.sn)
+ except Exception as e: # not an Apple serial number
+ return render(request, "checkin/device_form.html", locals())
+
+ try:
+ device = get_device(request, device.sn)
+ device_form = DeviceForm(instance=device)
+ except GsxError as e:
+ error = e
+
+ return render(request, "checkin/device_form.html", locals())
+
+ return render(request, "checkin/newindex.html", locals())
diff --git a/servo/views/customer.py b/servo/views/customer.py
new file mode 100644
index 0000000..455126e
--- /dev/null
+++ b/servo/views/customer.py
@@ -0,0 +1,505 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from __future__ import absolute_import
+
+from django.db.models import Q
+from django.contrib import messages
+from django.http import HttpResponse
+
+from django.forms.models import modelform_factory
+from django.utils.translation import ugettext as _
+from django.contrib.auth.decorators import permission_required
+from django.shortcuts import render, redirect, get_object_or_404
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models.note import Note
+from servo.models.order import Order
+from servo.models.common import Property
+from servo.models.customer import Customer, CustomerGroup, ContactInfo
+
+from servo.forms.customer import (CustomerForm,
+ CustomerSearchForm,
+ CustomerUploadForm)
+
+GroupForm = modelform_factory(CustomerGroup, exclude=[])
+
+
+def prepare_view(request, group='all'):
+
+ title = _("Customers")
+
+ customer_list = []
+ all_customers = Customer.objects.all().order_by('name')
+ customer_count = all_customers.count()
+
+ if request.session.get("return_to"):
+ del(request.session['return_to'])
+
+ if request.method == 'POST':
+ q = request.POST.get('query')
+ if q is not None:
+ try:
+ (key, value) = q.split('=')
+ # allow searching customers by arbitrary key/values
+ customer_list = Customer.objects.filter(**{key: value.strip()})
+ except Exception:
+ customer_list = Customer.objects.filter(name__icontains=q)
+ else:
+ if group == 'all':
+ customer_list = all_customers
+ else:
+ g = CustomerGroup.objects.get(slug=group)
+ customer_list = all_customers.filter(groups=g)
+ title = g.name
+
+ page = request.GET.get('page')
+ paginator = Paginator(customer_list, 40)
+
+ try:
+ customers = paginator.page(page)
+ except PageNotAnInteger:
+ customers = paginator.page(1)
+ except EmptyPage:
+ customers = paginator.page(paginator.num_pages)
+
+ groups = CustomerGroup.objects.all()
+
+ return locals()
+
+
+def index(request, group='all'):
+ data = prepare_view(request, group)
+ request.session['customer_query'] = None
+
+ if data['customer_list']:
+ customer = data['customer_list'][0]
+ return redirect(view, pk=customer.pk, group=group)
+
+ return render(request, "customers/index.html", data)
+
+
+@permission_required("servo.change_order")
+def add_order(request, customer_id, order_id):
+ order = Order.objects.get(pk=order_id)
+ customer = Customer.objects.get(pk=customer_id)
+ order.customer = customer
+ order.save()
+
+ for d in order.devices.all():
+ customer.devices.add(d)
+
+ customer.save()
+ messages.success(request, _('Customer added'))
+ return redirect(order)
+
+
+def notes(request, pk, note_id=None):
+ from servo.forms.note import NoteForm
+ customer = Customer.objects.get(pk=pk)
+ form = NoteForm(initial={'recipient': customer.name})
+
+ return render(request, "notes/form.html", {'form': form})
+
+
+def view(request, pk, group='all'):
+ try:
+ c = Customer.objects.get(pk=pk)
+ except Customer.DoesNotExist:
+ messages.error(request, _('Customer not found'))
+ return redirect(index)
+
+ data = prepare_view(request, group)
+
+ data['title'] = c.name
+ data['orders'] = Order.objects.filter(
+ customer__lft__gte=c.lft,
+ customer__rght__lte=c.rght,
+ customer__tree_id=c.tree_id
+ )
+
+ if c.email:
+ data['notes'] = Note.objects.filter(recipient=c.email)
+
+ data['customer'] = c
+ request.session['return_to'] = request.path
+
+ return render(request, 'customers/view.html', data)
+
+
+@permission_required("servo.change_customer")
+def edit_group(request, group='all'):
+ if group == 'all':
+ group = CustomerGroup()
+ else:
+ group = CustomerGroup.objects.get(slug=group)
+
+ title = group.name
+ form = GroupForm(instance=group)
+
+ if request.method == "POST":
+ form = GroupForm(request.POST, instance=group)
+ if form.is_valid():
+ group = form.save()
+ messages.success(request, _(u'%s saved') % group.name)
+ return redirect(index, group.slug)
+ messages.error(request, form.errors['name'][0])
+ return redirect(index)
+
+ return render(request, "customers/edit_group.html", locals())
+
+
+@permission_required("servo.change_customer")
+def delete_group(request, group):
+ group = CustomerGroup.objects.get(slug=group)
+
+ if request.method == "POST":
+ group.delete()
+ messages.success(request, _(u'%s deleted') % group.name)
+ return redirect(index)
+
+ return render(request, "customers/delete_group.html", locals())
+
+
+@permission_required("servo.change_customer")
+def edit(request, pk=None, parent_id=None, group='all'):
+
+ data = prepare_view(request, group)
+
+ customer = Customer()
+ form = CustomerForm(instance=customer)
+
+ if group != 'all':
+ g = CustomerGroup.objects.get(slug=group)
+ form.initial = {'groups': [g]}
+
+ name = request.GET.get('name')
+
+ if name:
+ form = CustomerForm(initial={'name': name})
+
+ if pk is not None:
+ customer = Customer.objects.get(pk=pk)
+ form = CustomerForm(instance=customer)
+
+ if parent_id is not None:
+ customer.parent = Customer.objects.get(pk=parent_id)
+ form = CustomerForm(initial={'parent': parent_id})
+
+ if request.method == 'POST':
+ props = dict()
+ keys = request.POST.getlist('keys')
+ values = request.POST.getlist('values')
+
+ form = CustomerForm(request.POST, request.FILES, instance=customer)
+
+ if form.is_valid():
+ ContactInfo.objects.filter(customer=customer).delete()
+
+ for k, v in enumerate(values):
+ if v != '':
+ key = keys[k]
+ props[key] = v
+
+ if form.is_valid():
+ try:
+ customer = form.save()
+ except Exception as e:
+ messages.error(request, e)
+ return redirect(edit, group, pk)
+
+ for k, v in props.items():
+ if v != '':
+ ContactInfo.objects.create(key=k, value=v, customer=customer)
+
+ messages.success(request, _('Customer saved'))
+
+ if request.session.get('return_to'):
+ return_to = request.session['return_to']
+ if hasattr(return_to, 'set_customer'):
+ return_to.set_customer(customer)
+ del request.session['return_to']
+ return redirect(return_to)
+
+ return redirect(view, pk=customer.pk, group=group)
+
+ data['form'] = form
+ data['customer'] = customer
+ data['title'] = customer.name
+ data['fields'] = Property.objects.filter(type='customer')
+
+ return render(request, 'customers/form.html', data)
+
+
+@permission_required("servo.delete_customer")
+def delete(request, pk=None, group='all'):
+
+ customer = Customer.objects.get(pk=pk)
+
+ if request.method == "POST":
+ customer.delete()
+ messages.success(request, _("Customer deleted"))
+ return redirect(index, group=group)
+ else:
+ data = {'action': request.path, 'customer': customer}
+ return render(request, "customers/remove.html", data)
+
+
+@permission_required("servo.change_customer")
+def merge(request, pk, target=None):
+ """
+ Merges customer PK with customer TARGET
+ Re-links everything from customer PK to TARGET:
+ - orders
+ - devices
+ - invoices
+ Deletes the source customer
+ """
+ customer = Customer.objects.get(pk=pk)
+ title = _('Merge %s with') % customer.name
+
+ if request.method == 'POST':
+ name = request.POST.get('name')
+ results = Customer.objects.filter(name__icontains=name)
+ return render(request, 'customers/results-merge.html', locals())
+
+ if pk and target:
+ target_customer = Customer.objects.get(pk=target)
+ target_customer.orders.add(*customer.orders.all())
+ target_customer.devices.add(*customer.devices.all())
+ target_customer.note_set.add(*customer.note_set.all())
+ target_customer.invoice_set.add(*customer.invoice_set.all())
+ target_customer.save()
+ customer.delete()
+ messages.success(request, _('Customer records merged succesfully'))
+ return redirect(target_customer)
+
+ return render(request, "customers/merge.html", locals())
+
+
+@permission_required("servo.change_customer")
+def move(request, pk, new_parent=None):
+ """
+ Moves a customer under another customer
+ """
+ customer = Customer.objects.get(pk=pk)
+
+ if new_parent is not None:
+ if int(new_parent) == 0:
+ new_parent = None
+ msg = _(u"Customer %s moved to top level") % customer
+ else:
+ new_parent = Customer.objects.get(pk=new_parent)
+ d = {'customer': customer, 'target': new_parent}
+ msg = _(u"Customer %(customer)s moved to %(target)s") % d
+
+ try:
+ customer.move_to(new_parent)
+ customer.save() # To update fullname
+ messages.success(request, msg)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(customer)
+
+ return render(request, "customers/move.html", locals())
+
+
+def search(request):
+ """
+ Searches for customers from "spotlight"
+ """
+ query = request.GET.get("q")
+ kind = request.GET.get('kind')
+ request.session['search_query'] = query
+
+ customers = Customer.objects.filter(
+ Q(fullname__icontains=query) | Q(email__icontains=query) | Q(phone__contains=query)
+ )
+
+ if kind == 'company':
+ customers = customers.filter(is_company=True)
+
+ if kind == 'contact':
+ customers = customers.filter(is_company=False)
+
+ title = _('Search results for "%s"') % query
+ return render(request, "customers/search.html", locals())
+
+
+def filter(request):
+ """
+ Search for customers by name
+ May return JSON for ajax requests
+ or a rendered list
+ """
+ import json
+ from django.http import HttpResponse
+
+ if request.method == "GET":
+ results = list()
+ query = request.GET.get("query")
+ customers = Customer.objects.filter(fullname__icontains=query)
+
+ for c in customers:
+ results.append(u"%s <%s>" % (c.name, c.email))
+ results.append(u"%s <%s>" % (c.name, c.phone))
+ else:
+ query = request.POST.get("name")
+ results = Customer.objects.filter(fullname__icontains=query)
+ data = {'results': results, 'id': request.POST['id']}
+
+ return render(request, "customers/search-results.html", data)
+
+ return HttpResponse(json.dumps(results), content_type="application/json")
+
+
+def find(request):
+ """
+ Search from customer advanced search
+ """
+ results = list()
+ request.session['customer_list'] = list()
+
+ if request.method == 'POST':
+ form = CustomerSearchForm(request.POST)
+
+ if form.is_valid():
+ d = form.cleaned_data
+ checkin_start = d.pop('checked_in_start')
+ checkin_end = d.pop('checked_in_end')
+
+ if checkin_start and checkin_end:
+ d['orders__created_at__range'] = [checkin_start.isoformat(),
+ checkin_end.isoformat()]
+
+ results = Customer.objects.filter(**d).distinct()
+ request.session['customer_query'] = d
+ else:
+ form = CustomerSearchForm()
+
+ title = _('Search for customers')
+
+ page = request.GET.get('page')
+ paginator = Paginator(results, 50)
+
+ try:
+ customers = paginator.page(page)
+ except PageNotAnInteger:
+ customers = paginator.page(1)
+ except EmptyPage:
+ customers = paginator.page(paginator.num_pages)
+
+ return render(request, "customers/find.html", locals())
+
+
+def download(request, format='csv', group='all'):
+ """
+ Downloads all customers or search results
+ """
+ filename = 'customers'
+ results = Customer.objects.all()
+ query = request.session.get('customer_query')
+
+ response = HttpResponse(content_type="text/plain; charset=utf-8")
+ response['Content-Disposition'] = 'attachment; filename="%s.txt"' % filename
+ response.write(u"ID\tNAME\tEMAIL\tPHONE\tADDRESS\tPOSTAL CODE\tCITY\tCOUNTRY\tNOTES\n")
+
+ if group != 'all':
+ results = results.filter(groups__slug=group)
+
+ if query:
+ results = Customer.objects.filter(**query).distinct()
+
+ for c in results:
+ row = u"%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (c.pk,
+ c.name,
+ c.email,
+ c.phone,
+ c.street_address,
+ c.zip_code,
+ c.city,
+ c.country,
+ c.notes,)
+ response.write(row)
+
+ return response
+
+
+def create_message(request, pk):
+ return redirect("servo.views.note.edit", customer=pk)
+
+
+def upload(request, group='all'):
+
+ action = request.path
+ form = CustomerUploadForm()
+
+ if request.method == 'POST':
+ form = CustomerUploadForm(request.POST, request.FILES)
+
+ if not form.is_valid():
+ messages.error(request, form.errors)
+ return redirect(index)
+
+ i, df = 0, form.cleaned_data['datafile'].read()
+
+ for l in df.split("\r"):
+ row = force_decode(l).strip().split("\t")
+
+ if len(row) < 5:
+ messages.error(request, _("Invalid upload data"))
+ return redirect(index)
+
+ if form.cleaned_data.get('skip_dups'):
+ if Customer.objects.filter(email=row[1]).exists():
+ continue
+
+ c = Customer(name=row[0], email=row[1])
+ c.street_address = row[2]
+ c.zip_code = row[3]
+ c.city = row[4]
+ c.notes = row[5]
+ c.save()
+
+ if group != 'all':
+ g = CustomerGroup.objects.get(slug=group)
+ c.groups.add(g)
+
+ i += 1
+
+ messages.success(request, _("%d customer(s) imported") % i)
+ return redirect(index, group=group)
+
+ return render(request, "customers/upload.html", locals())
+
+
+def force_decode(s, codecs=['mac_roman', 'utf-8', 'latin-1']):
+ for i in codecs:
+ try:
+ return s.decode(i)
+ except UnicodeDecodeError:
+ pass
diff --git a/servo/views/device.py b/servo/views/device.py
new file mode 100644
index 0000000..f35bd99
--- /dev/null
+++ b/servo/views/device.py
@@ -0,0 +1,605 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+
+from django.db.models import Q
+from django.contrib import messages
+
+from django.core.cache import cache
+from django.shortcuts import render, redirect, get_object_or_404
+
+from django.utils.translation import ugettext as _
+from django.template.defaultfilters import slugify
+from django.views.decorators.cache import cache_page
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models import Device, Product, GsxAccount, ServiceOrderItem
+from servo.forms.devices import DeviceForm, DeviceUploadForm, DeviceSearchForm
+
+class RepairDiagnosticResults:
+ pass
+
+class DiagnosticResults(object):
+ def __init__(self, diags):
+ if not diags.diagnosticTestData:
+ raise gsxws.GsxError('Missing diagnostic data')
+
+ self.diags = dict(result={}, profile={}, report={})
+
+ for r in diags.diagnosticTestData.testResult.result:
+ self.diags['result'][r.name] = r.value
+
+ for r in diags.diagnosticProfileData.profile.unit.key:
+ self.diags['profile'][r.name] = r.value
+
+ for r in diags.diagnosticProfileData.report.reportData.key:
+ self.diags['report'][r.name] = r.value
+
+ def __iter__(self):
+ return iter(self.diags)
+
+
+def model_from_slug(product_line, model=None):
+ """
+ Returns product description for model slug or models dict for
+ the specified product line
+ """
+ if not cache.get("slugmap"):
+ slugmap = {} # Map model slug to corresponding product description
+ product_lines = gsxws.products.models()
+
+ for k, v in product_lines.items():
+ d = {}
+ for p in v['models']:
+ slug = slugify(p)
+ d[slug] = p
+
+ slugmap[k] = d
+
+ cache.set("slugmap", slugmap)
+
+ models = cache.get("slugmap").get(product_line)
+
+ if model is not None:
+ return models.get(model)
+
+ return models
+
+
+def prep_list_view(request, product_line=None, model=None):
+ title = _('Devices')
+ all_devices = Device.objects.all()
+ product_lines = gsxws.products.models()
+
+ if product_line is None:
+ product_line = product_lines.keys()[0]
+
+ models = model_from_slug(product_line)
+
+ if model is None:
+ model = models.keys()[0]
+ title = product_lines[product_line]['name']
+ else:
+ title = models.get(model)
+
+ if product_line == "OTHER":
+ all_devices = all_devices.filter(product_line=product_line)
+ else:
+ all_devices = all_devices.filter(slug=model)
+
+ page = request.GET.get('page')
+ paginator = Paginator(all_devices, 50)
+
+ try:
+ devices = paginator.page(page)
+ except PageNotAnInteger:
+ devices = paginator.page(1)
+ except EmptyPage:
+ devices = paginator.page(paginator.num_pages)
+
+ return locals()
+
+
+def prep_detail_view(request, pk, product_line=None, model=None):
+ if pk is None:
+ device = Device()
+ else:
+ device = Device.objects.get(pk=pk)
+
+ data = prep_list_view(request, product_line, model)
+
+ data['device'] = device
+ data['title'] = device.description
+
+ return data
+
+
+def index(request, product_line=None, model=None):
+ if request.session.get('return_to'):
+ del(request.session['return_to'])
+
+ data = prep_list_view(request, product_line, model)
+
+ if data['all_devices'].count() > 0:
+ return redirect(data['all_devices'].latest())
+
+ return render(request, "devices/index.html", data)
+
+
+def delete_device(request, product_line, model, pk):
+ dev = Device.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ from django.db.models import ProtectedError
+ try:
+ dev.delete()
+ messages.success(request, _("Device deleted"))
+ except ProtectedError:
+ messages.error(request, _("Cannot delete device with GSX repairs"))
+ return redirect(dev)
+
+ return redirect(index)
+
+ data = {'action': request.path}
+ data['device'] = dev
+
+ return render(request, "devices/remove.html", data)
+
+
+def edit_device(request, pk=None, product_line=None, model=None):
+ """
+ Edits an existing device or adds a new one
+ """
+ device = Device()
+ device.sn = request.GET.get('sn', '')
+
+ if product_line is not None:
+ device.product_line = product_line
+
+ if model is not None:
+ device.product_line = product_line
+ device.description = model_from_slug(product_line, model)
+
+ if pk is not None:
+ device = Device.objects.get(pk=pk)
+
+ form = DeviceForm(instance=device)
+
+ if request.method == "POST":
+
+ form = DeviceForm(request.POST, request.FILES, instance=device)
+
+ if form.is_valid():
+ device = form.save()
+ messages.success(request, _(u"%s saved") % device.description)
+ device.add_tags(request.POST.getlist('tag'))
+
+ return redirect(view_device,
+ pk=device.pk,
+ product_line=device.product_line,
+ model=device.slug)
+
+ data = prep_detail_view(request, pk, product_line, model)
+ data['form'] = form
+
+ return render(request, 'devices/form.html', data)
+
+
+def view_device(request, pk, product_line=None, model=None):
+ data = prep_detail_view(request, pk, product_line, model)
+ return render(request, "devices/view.html", data)
+
+
+def diagnostics(request, pk):
+ """
+ Fetches MRI diagnostics or initiates iOS diags from GSX
+ """
+ device = get_object_or_404(Device, pk=pk)
+
+ if request.GET.get('a') == 'init':
+ if request.method == 'POST':
+ from gsxws import diagnostics
+ order = request.POST.get('order')
+ order = device.order_set.get(pk=order)
+ email = request.POST.get('email')
+ diag = diagnostics.Diagnostics(serialNumber=device.sn)
+ diag.emailAddress = email
+ diag.shipTo = order.location.gsx_shipto
+
+ try:
+ GsxAccount.default(request.user)
+ res = diag.initiate()
+ msg = _('Diagnostics initiated - diags://%s') % res
+ order.notify("init_diags", msg, request.user)
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(order)
+
+ order = request.GET.get('order')
+ order = device.order_set.get(pk=order)
+ customer = order.customer
+ url = request.path
+ return render(request, "devices/diagnostic_init.html", locals())
+
+ if request.GET.get('a') == 'get':
+ try:
+ diagnostics = device.get_diagnostics(request.user)
+ if device.is_ios():
+ diagnostics = DiagnosticResults(diagnostics)
+ return render(request, "devices/diagnostic_ios.html", locals())
+ return render(request, "devices/diagnostic_results.html", locals())
+ except gsxws.GsxError, e:
+ return render(request, "devices/diagnostic_error.html", {'error': e})
+
+ return render(request, "devices/diagnostics.html", locals())
+
+
+def get_gsx_search_results(request, what, param, query):
+ """
+ The second phase of a GSX search.
+ There should be an active GSX session open at this stage.
+ """
+ data = {}
+ results = []
+ query = query.upper()
+ device = Device(sn=query)
+ error_template = "search/results/gsx_error.html"
+
+ # @TODO: this isn't a GSX search. Move it somewhere else.
+ if what == "orders":
+ try:
+ if param == 'serialNumber':
+ device = Device.objects.get(sn__exact=query)
+ if param == 'alternateDeviceId':
+ device = Device.objects.get(imei__exact=query)
+ except (Device.DoesNotExist, ValueError,):
+ return render(request, "search/results/gsx_notfound.html")
+
+ orders = device.order_set.all()
+ return render(request, "orders/list.html", locals())
+
+ if what == "warranty":
+ # Update wty info if been here before
+ try:
+ device = Device.objects.get(sn__exact=query)
+ device.update_gsx_details()
+ except Exception:
+ try:
+ device = Device.from_gsx(query)
+ except Exception, e:
+ return render(request, error_template, {'message': e})
+
+ results.append(device)
+
+ # maybe it's a device we've already replaced...
+ try:
+ soi = ServiceOrderItem.objects.get(sn__iexact=query)
+ results[0].repeat_service = soi.order
+ except ServiceOrderItem.DoesNotExist:
+ pass
+
+ if what == "parts":
+ # looking for parts
+ if param == "partNumber":
+ # ... with a part number
+ part = gsxws.Part(partNumber=query)
+
+ try:
+ partinfo = part.lookup()
+ except gsxws.GsxError, e:
+ return render(request, error_template, {'message': e})
+
+ product = Product.from_gsx(partinfo)
+ cache.set(query, product)
+ results.append(product)
+
+ if param == "serialNumber":
+ # ... with a serial number
+ try:
+ results = device.get_parts()
+ data['device'] = device
+ except Exception, e:
+ return render(request, error_template, {'message': e})
+
+ if param == "productName":
+ product = gsxws.Product(productName=query)
+ parts = product.parts()
+ for p in parts:
+ results.append(Product.from_gsx(p))
+
+ if what == "repairs":
+ # Looking for GSX repairs
+ if param == "serialNumber":
+ # ... with a serial number
+ try:
+ device = gsxws.Product(query)
+ #results = device.repairs()
+ # @TODO: move the encoding hack to py-gsxws
+ for i, p in enumerate(device.repairs()):
+ d = {'purchaseOrderNumber': p.purchaseOrderNumber}
+ d['repairConfirmationNumber'] = p.repairConfirmationNumber
+ d['createdOn'] = p.createdOn
+ d['customerName'] = p.customerName.encode('utf-8')
+ d['repairStatus'] = p.repairStatus
+ results.append(d)
+ except gsxws.GsxError, e:
+ return render(request, "search/results/gsx_notfound.html")
+
+ elif param == "dispatchId":
+ # ... with a repair confirmation number
+ repair = gsxws.Repair(number=query)
+ try:
+ results = repair.lookup()
+ except gsxws.GsxError, message:
+ return render(request, error_template, locals())
+
+ return render(request, "devices/search_gsx_%s.html" % what, locals())
+
+
+def search_gsx(request, what, param, query):
+ """
+ The first phase of a GSX search
+ """
+ title = _(u'Search results for "%s"') % query
+
+ try:
+ act = request.session.get("gsx_account")
+ act = None
+ if act is None:
+ GsxAccount.default(user=request.user)
+ else:
+ act.connect(request.user)
+ except gsxws.GsxError, message:
+ return render(request, "devices/search_gsx_error.html", locals())
+
+ if request.is_ajax():
+ if what == "parts":
+ try:
+ dev = Device.from_gsx(query)
+ products = dev.get_parts()
+ return render(request, "devices/parts.html", locals())
+ except gsxws.GsxError, message:
+ return render(request, "search/results/gsx_error.html", locals())
+
+ return get_gsx_search_results(request, what, param, query)
+
+ return render(request, "devices/search_gsx.html", locals())
+
+
+def search(request):
+ """
+ Searching for devices from the main navbar
+ """
+ query = request.GET.get("q", '').strip()
+ request.session['search_query'] = query
+
+ query = query.upper()
+ valid_arg = gsxws.validate(query)
+
+ if valid_arg in ('serialNumber', 'alternateDeviceId',):
+ return redirect(search_gsx, "warranty", valid_arg, query)
+
+ devices = Device.objects.filter(
+ Q(sn__icontains=query) | Q(description__icontains=query)
+ )
+
+ title = _(u'Devices matching "%s"') % query
+
+ return render(request, "devices/search.html", locals())
+
+
+def find(request):
+ """
+ Searching for device from devices/find
+ """
+ title = _("Device search")
+ form = DeviceSearchForm()
+ results = Device.objects.none()
+
+ if request.method == 'POST':
+ form = DeviceSearchForm(request.POST)
+ if form.is_valid():
+ fdata = form.cleaned_data
+ results = Device.objects.all()
+
+ if fdata.get("product_line"):
+ results = results.filter(product_line__in=fdata['product_line'])
+ if fdata.get("warranty_status"):
+ results = results.filter(warranty_status__in=fdata['warranty_status'])
+ if fdata.get("description"):
+ results = results.filter(description__icontains=fdata['description'])
+ if fdata.get("sn"):
+ results = results.filter(sn__icontains=fdata['sn'])
+ if fdata.get("date_start"):
+ results = results.filter(created_at__range=[fdata['date_start'],
+ fdata['date_end']])
+
+ paginator = Paginator(results, 100)
+ page = request.GET.get("page")
+
+ try:
+ devices = paginator.page(page)
+ except PageNotAnInteger:
+ devices = paginator.page(1)
+ except EmptyPage:
+ devices = paginator.page(paginator.num_pages)
+
+ return render(request, "devices/find.html", locals())
+
+
+#@cache_page(60*5)
+def parts(request, pk, order_id, queue_id):
+ """
+ Lists available parts for this device/order
+ taking into account the order's queues GSX Sold-To
+ and the Location's corresponding GSX account
+ """
+ from decimal import InvalidOperation
+
+ device = Device.objects.get(pk=pk)
+ order = device.order_set.get(pk=order_id)
+
+ try:
+ # remember the right GSX account
+ act = GsxAccount.default(request.user, order.queue)
+ request.session['gsx_account'] = act.pk
+ products = device.get_parts()
+ except gsxws.GsxError as message:
+ return render(request, "search/results/gsx_error.html", locals())
+ except AttributeError:
+ message = _('Invalid serial number for parts lookup')
+ return render(request, "search/results/gsx_error.html", locals())
+ except InvalidOperation:
+ message = _('Error calculating prices. Please check your system settings.')
+ return render(request, "search/results/gsx_error.html", locals())
+
+ return render(request, "devices/parts.html", locals())
+
+
+def model_parts(request, product_line=None, model=None):
+ """
+ Shows parts for this device model
+ """
+ data = prep_list_view(request, product_line, model)
+
+ if cache.get("slugmap") and model:
+ models = cache.get("slugmap")[product_line]
+ data['what'] = "parts"
+ data['param'] = "productName"
+ data['query'] = models[model]
+ data['products'] = Product.objects.filter(tags__tag=data['query'])
+
+ return render(request, "devices/index.html", data)
+
+
+def choose(request, order_id):
+ """
+ Choosing a device from within an SRO
+ Does GSX lookup in case device is not found locally
+ """
+ context = {'order': order_id}
+
+ if request.method == "POST":
+
+ query = request.POST.get('q').upper()
+ results = Device.objects.filter(Q(sn__iexact=query) | Q(imei=query))
+
+ if len(results) < 1:
+ try:
+ current_order = request.session.get("current_order_id")
+ current_order = Order.objects.get(pk=current_order)
+ if current_order and current_order.queue:
+ GsxAccount.default(request.user, current_order.queue)
+ else:
+ GsxAccount.default(request.user)
+ results = [Device.from_gsx(query)]
+ except Exception as e:
+ context['error'] = e
+ return render(request, "devices/choose-error.html", context)
+
+ context['results'] = results
+ return render(request, "devices/choose-list.html", context)
+
+ return render(request, "devices/choose.html", context)
+
+
+def upload_devices(request):
+ """
+ User uploads device DB as tab-delimited CSV file
+ SN USERNAME PASSWORD NOTES
+ """
+ gsx_account = None
+ form = DeviceUploadForm()
+
+ if request.method == "POST":
+ form = DeviceUploadForm(request.POST, request.FILES)
+
+ if form.is_valid():
+ i = 0
+ df = form.cleaned_data['datafile'].read()
+
+ if form.cleaned_data.get('do_warranty_check'):
+ gsx_account = GsxAccount.default(request.user)
+
+ for l in df.split("\r"):
+ l = l.decode("latin-1").encode("utf-8")
+ row = l.strip().split("\t")
+
+ if gsx_account:
+ try:
+ device = Device.from_gsx(row[0])
+ except Exception, e:
+ messages.error(request, e)
+ break
+ else:
+ device = Device.objects.get_or_create(sn=row[0])[0]
+
+ try:
+ device.username = row[1]
+ device.password = row[2]
+ device.notes = row[3]
+ except IndexError:
+ pass
+
+ device.save()
+ i += 1
+
+ if form.cleaned_data.get("customer"):
+ customer = form.cleaned_data['customer']
+ customer.devices.add(device)
+
+ messages.success(request, _("%d devices imported") % i)
+
+ return redirect(index)
+
+ data = {'form': form, 'action': request.path}
+ return render(request, "devices/upload_devices.html", data)
+
+
+def update_gsx_details(request, pk):
+ """
+ Updates devices GSX warranty details
+ """
+ device = get_object_or_404(Device, pk=pk)
+ try:
+ GsxAccount.default(request.user)
+ device.update_gsx_details()
+ messages.success(request, _("Warranty status updated successfully"))
+ except Exception, e:
+ messages.error(request, e)
+
+ if request.session.get('return_to'):
+ return redirect(request.session['return_to'])
+
+ return redirect(device)
+
+
+def get_info(request, pk):
+ device = get_object_or_404(Device, pk=pk)
+ return render(request, "devices/get_info.html", locals())
diff --git a/servo/views/error.py b/servo/views/error.py
new file mode 100644
index 0000000..b05367d
--- /dev/null
+++ b/servo/views/error.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django import forms
+from django.conf import settings
+from django.core.mail import send_mail
+from servo.lib.shorturl import from_time
+from servo.forms.base import FullTextArea
+from django.shortcuts import render
+from django.utils.translation import ugettext as _
+
+
+class ErrorForm(forms.Form):
+ description = FullTextArea(max_length=512, min_length=10)
+
+
+def report(request):
+ crashed = True
+ if request.method == 'POST':
+ form = ErrorForm(request.POST)
+ if form.is_valid():
+ ref = 'Error %s' % from_time()
+ recipient = settings.ADMINS[0][1]
+ send_mail(ref, form.cleaned_data['description'], request.user.email, [recipient])
+ crashed = False
+ else:
+ initial = _('Browser: %s') % request.META['HTTP_USER_AGENT']
+ form = ErrorForm(initial={'description': initial})
+
+ return render(request, 'error.html', locals())
diff --git a/servo/views/events.py b/servo/views/events.py
new file mode 100644
index 0000000..66ded84
--- /dev/null
+++ b/servo/views/events.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.utils import timezone
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+
+from servo.models.common import Event
+
+
+def acknowledge(request, pk):
+ e = Event.objects.get(pk=pk)
+ e.handled_at = timezone.now()
+ e.save()
+
+ referer = request.META.get('HTTP_REFERER')
+
+ if request.GET.get('return') == '0'and referer:
+ return redirect(referer)
+
+ return redirect(e.content_object.get_absolute_url())
diff --git a/servo/views/files.py b/servo/views/files.py
new file mode 100644
index 0000000..6c3002f
--- /dev/null
+++ b/servo/views/files.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import os
+import mimetypes
+from django.conf import settings
+from django.http import HttpResponse, Http404
+
+from servo.models.common import Attachment
+
+
+def view_file(request, pk):
+ doc = Attachment.objects.get(pk=pk)
+ return HttpResponse(doc.content.read(), content_type=doc.mime_type)
+
+
+def get_file(request, path):
+ """
+ Returns a file from the upload directory
+ """
+ try:
+ f = open(os.path.join(settings.MEDIA_ROOT, path), 'r')
+ except IOError:
+ raise Http404
+
+ mimetypes.init()
+ t, e = mimetypes.guess_type(f.name)
+
+ return HttpResponse(f.read(), t)
diff --git a/servo/views/gsx.py b/servo/views/gsx.py
new file mode 100644
index 0000000..c9af1a1
--- /dev/null
+++ b/servo/views/gsx.py
@@ -0,0 +1,349 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+import logging
+
+from django.contrib import messages
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib.auth.decorators import permission_required
+
+from servo.models import Order, GsxAccount, Repair, ServicePart
+from servo.forms import GsxCustomerForm, GsxRepairForm, GsxComponentForm
+
+
+class RepairDetails(object):
+ def __init__(self, confirmation):
+ repair = gsxws.Repair(confirmation).details()
+ self.dispatch_id = repair.dispatchId
+ self.po_number = repair.purchaseOrderNumber
+ self.cs_code = repair.csCode
+ self.tracking_number = repair.deliveryTrackingNumber
+ self.notes = repair.notes
+ self.status = repair.repairStatus
+ self.status_description = repair.coverageStatusDescription
+ self.parts = repair.partsInfo
+
+
+@permission_required("servo.change_order")
+def register_return(request, part_id):
+ part = ServicePart.objects.get(pk=part_id)
+ try:
+ part.register_for_return(request.user)
+ messages.success(request, _(u"Part %s updated") % part.order_item.code)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(part.repair.order)
+
+
+@permission_required("servo.change_repair")
+def import_repair(request, pk):
+ pass
+
+
+@permission_required("servo.change_order")
+def return_label(request, repair, part):
+ """
+ Returns the return label PDF for this repair and part
+ """
+ repair = Repair.objects.get(pk=repair)
+
+ try:
+ repair.connect_gsx(request.user)
+ label_data = repair.get_return_label(part)
+ return HttpResponse(label_data, content_type="application/pdf")
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+ return redirect(repair.order)
+
+
+@permission_required("servo.change_repair")
+def add_part(request, repair, part):
+ """
+ Adds this part to this GSX repair
+ """
+ rep = Repair.objects.get(pk=repair)
+ soi = rep.order.serviceorderitem_set.get(pk=part)
+
+ if request.method == "POST":
+ try:
+ part = rep.add_part(soi, request.user)
+ data = {'part': part.part_number, 'repair': rep.confirmation}
+ msg = _("Part %(part)s added to repair %(repair)s") % data
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(rep.order)
+
+ context = {'item': soi}
+ context['repair'] = rep
+ context['action'] = request.path
+
+ return render(request, "repairs/add_part.html", context)
+
+
+def remove_part(request, repair, part):
+ rep = Repair.objects.get(pk=repair)
+ part = ServicePart.objects.get(pk=part)
+
+ if request.method == "POST":
+
+ rep.connect_gsx(request.user)
+ gsx_rep = rep.get_gsx_repair()
+ orderline = part.get_repair_order_line()
+ orderline.toDelete = True
+ orderline.orderLineNumber = part.line_number
+
+ try:
+ gsx_rep.update({'orderLines': [orderline]})
+ data = {'part': part.code, 'repair': rep.confirmation}
+ msg = _(u"Part %(part)s removed from %(repair)s") % data
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(rep.order)
+
+ data = {'action': request.path}
+ return render(request, "repairs/delete_part.html", data)
+
+
+def delete_repair(request, repair_id):
+ repair = get_object_or_404(Repair, pk=repair_id)
+ if repair.submitted_at:
+ messages.error(request, _('Submitted repairs cannot be deleted'))
+ return redirect(repair.order)
+
+ if request.method == 'POST':
+ order = repair.order
+ repair.delete()
+ messages.success(request, _('GSX repair deleted'))
+ return redirect(order)
+
+ context = {'action': request.path}
+ return render(request, 'repairs/delete_repair.html', context)
+
+
+def check_parts_warranty(request, repair):
+ """
+ Checks this (new) repair warranty status
+ with the included device and parts
+ """
+ repair = Repair.objects.get(pk=repair)
+ parts = repair.order.get_parts()
+
+ try:
+ wty = repair.warranty_status()
+ wty_parts = wty.parts
+ except Exception, e:
+ return render(request, 'search/results/gsx_error.html', {'message': e})
+
+ try:
+ for k, v in enumerate(parts):
+ try:
+ parts[k].warranty_status = wty_parts[k].partWarranty
+ except TypeError:
+ parts[k].warranty_status = _('Unknown')
+ except KeyError:
+ parts[0].warranty_status = wty_parts.partWarranty
+
+ context = {'parts': parts}
+ context['checked_parts'] = [p.pk for p in repair.parts.all()]
+ return render(request, 'repairs/check_parts.html', context)
+
+
+def prep_edit_view(request, repair, order=None, device=None):
+ """
+ Prepares edit view for GSX repair
+ """
+ context = {'order': order}
+
+ if repair.submitted_at:
+ raise ValueError(_("Submitted repairs cannot be edited"))
+
+ if not order.has_parts:
+ raise ValueError(_("Please add some parts before creating repair"))
+
+ if not order.customer:
+ raise ValueError(_("Cannot create GSX repair without valid customer data"))
+
+ customer = order.customer.gsx_address(request.user.location)
+ customer_form = GsxCustomerForm(initial=customer)
+
+ context['repair'] = repair
+ context['customer'] = customer
+ context['title'] = repair.get_number()
+ context['customer_form'] = customer_form
+ context['device'] = device or repair.device
+ context['repair_form'] = GsxRepairForm(instance=repair)
+
+ if len(repair.component_data):
+ context['component_form'] = GsxComponentForm(components=repair.component_data)
+
+ return context
+
+
+def edit_repair(request, order_id, repair_id):
+ """
+ Edits existing (non-submitted) GSX repair
+ """
+ order = Order.objects.get(pk=order_id)
+ repair = Repair.objects.get(pk=repair_id)
+ repair.set_parts(order.get_parts())
+
+ try:
+ repair.connect_gsx(request.user)
+ repair.check_components()
+ data = prep_edit_view(request, repair, order)
+ except (ValueError, gsxws.GsxError) as e:
+ messages.error(request, e)
+ return redirect(order)
+
+ if request.method == "POST":
+ try:
+ data = save_repair(request, data)
+ msg = _('GSX repair saved')
+ if 'confirm' in request.POST.keys():
+ repair.submit(data['customer_data'])
+ msg = _(u"GSX repair %s created") % repair.confirmation
+ messages.success(request, msg)
+ return redirect("repairs-view_repair", order.pk, repair.pk)
+ messages.success(request, msg)
+ return redirect(order)
+ except Exception, e:
+ messages.error(request, e)
+
+ return render(request, "orders/gsx_repair_form.html", data)
+
+
+def save_repair(request, context):
+ """
+ Saves this GSX repair
+ """
+ repair = context['repair']
+ customer = context['customer']
+
+ if len(repair.component_data):
+ component_form = GsxComponentForm(request.POST, components=repair.component_data)
+ if component_form.is_valid():
+ repair.component_data = component_form.json_data
+ else:
+ raise ValueError(_("Invalid component data"))
+
+ customer_form = GsxCustomerForm(request.POST, initial=customer)
+ repair_form = GsxRepairForm(request.POST, request.FILES, instance=repair)
+
+ if customer_form.is_valid():
+ context['customer_data'] = customer_form.cleaned_data
+ if repair_form.is_valid():
+ parts = repair_form.cleaned_data['parts']
+ repair.save()
+ repair.set_parts(parts)
+ else:
+ logging.debug(repair_form.errors)
+ raise ValueError(repair_form.errors)
+ else:
+ raise ValueError(_("Invalid customer info"))
+
+ context['repair_form'] = repair_form
+ context['customer_form'] = customer_form
+
+ return context
+
+
+def create_repair(request, order_id, device_id, type):
+ """
+ Creates a GSX repair for the specified SRO and device
+ and redirects to the repair's edit page.
+ """
+ from datetime import timedelta
+ from django.utils import timezone
+
+ order = Order.objects.get(pk=order_id)
+ device = order.devices.get(pk=device_id)
+
+ repair = Repair(order=order, created_by=request.user, device=device)
+ timediff = timezone.now() - order.created_at
+
+ if timediff.seconds <= 3600:
+ repair.unit_received_at = order.created_at - timedelta(hours=1)
+ else:
+ repair.unit_received_at = order.created_at
+
+ repair.reference = request.user.gsx_poprefix + order.code
+
+ try:
+ repair.gsx_account = GsxAccount.default(request.user, order.queue)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+
+ repair.repair_type = type
+ repair.tech_id = request.user.tech_id
+ repair.save()
+
+ return redirect(edit_repair, order.pk, repair.pk)
+
+
+def repair_details(request, confirmation):
+ """
+ Returns GSX repair details for confirmation number
+ """
+ repair = RepairDetails(confirmation)
+ data = {'repair': repair}
+ if request.method == "POST":
+ data = save_repair(request, data)
+ return render(request, "repairs/get_details.html", data)
+
+
+def copy_repair(request, pk):
+ """
+ Duplicates a local GSX repair
+ """
+ repair = Repair.objects.get(pk=pk)
+ new_repair = repair.duplicate(request.user)
+ return redirect(edit_repair, new_repair.order_id, new_repair.pk)
+
+
+def update_sn(request, pk, part):
+ """
+ Updates the parts serial number
+ """
+ part = ServicePart.objects.get(pk=part)
+
+ try:
+ part.repair.connect_gsx(request.user)
+ part.update_sn()
+ msg = _(u'%s serial numbers updated') % part.part_number
+ messages.success(request, msg)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(part.repair.order)
diff --git a/servo/views/invoices.py b/servo/views/invoices.py
new file mode 100644
index 0000000..cc48c60
--- /dev/null
+++ b/servo/views/invoices.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django import forms
+from django.utils import timezone
+from django.contrib import messages
+from django.utils.translation import ugettext as _
+from django.forms.models import inlineformset_factory
+from django.contrib.auth.decorators import permission_required
+from django.shortcuts import render, redirect, get_object_or_404
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.forms.invoices import *
+from servo.models import Order, Invoice, Payment, PurchaseOrder
+
+
+def invoices(request):
+ """
+ Lists invoices, optionally with a search filter
+ """
+ from datetime import timedelta
+ from django.db.models import Sum
+
+ data = {'title': _("Invoices")}
+ now = timezone.now()
+
+ start_date, end_date = now - timedelta(days=30), now
+ initial = {'start_date': start_date, 'end_date': end_date}
+
+ invoices = Invoice.objects.filter(created_at__range=(start_date, end_date))
+ form = InvoiceSearchForm(initial=initial)
+
+ if request.method == 'POST':
+ invoices = Invoice.objects.all()
+ form = InvoiceSearchForm(request.POST, initial=initial)
+
+ if form.is_valid():
+ fdata = form.cleaned_data
+ if fdata.get('state') == 'OPEN':
+ invoices = invoices.filter(paid_at=None)
+ if fdata.get('state') == 'PAID':
+ invoices = invoices.exclude(paid_at=None)
+
+ payment_method = fdata.get('payment_method')
+ if len(payment_method):
+ invoices = invoices.filter(payment__method=payment_method)
+
+ start_date = fdata.get('start_date', start_date)
+ end_date = fdata.get('end_date', end_date)
+ invoices = invoices.filter(created_at__range=(start_date, end_date))
+
+ if fdata.get('status_isnot'):
+ invoices = invoices.exclude(order__status__status=fdata['status_isnot'])
+
+ if fdata.get('customer_name'):
+ invoices = invoices.filter(customer_name__icontains=fdata['customer_name'])
+
+ if fdata.get('service_order'):
+ invoices = invoices.filter(order__code__exact=fdata['service_order'])
+
+ page = request.GET.get('page')
+ data['total'] = invoices.aggregate(Sum('total_net'))
+ data['total_paid'] = invoices.exclude(paid_at=None).aggregate(Sum('total_net'))
+ pos = PurchaseOrder.objects.filter(created_at__range=[start_date, end_date])
+ data['total_purchases'] = pos.aggregate(Sum('total'))
+
+ paginator = Paginator(invoices, 50)
+
+ try:
+ invoices = paginator.page(page)
+ except PageNotAnInteger:
+ invoices = paginator.page(1)
+ except EmptyPage:
+ invoices = paginator.page(paginator.num_pages)
+
+ data['form'] = form
+ data['invoices'] = invoices
+
+ return render(request, "invoices/index.html", data)
+
+
+def gsx_invoices(request):
+ pass
+
+
+def print_invoice(request, pk):
+ from servo.models import Configuration
+
+ invoice = get_object_or_404(Invoice, pk=pk)
+ template = invoice.order.get_print_template("receipt")
+
+ title = _("Receipt #%d") % invoice.pk
+ conf = Configuration.conf()
+ order = invoice.order
+
+ return render(request, template, locals())
+
+
+def view_invoice(request, pk):
+ title = _("Invoice %s") % pk
+ invoice = get_object_or_404(Invoice, pk=pk)
+ return render(request, "invoices/view_invoice.html", locals())
+
+
+@permission_required('servo.change_order')
+def create_invoice(request, order_id=None, numbers=None):
+ """
+ Dispatches Sales Order
+ """
+ order = get_object_or_404(Order, pk=order_id)
+ title = _(u'Dispatch Order %s') % order.code
+ products = order.products.filter(dispatched=False)
+
+ initial = {
+ 'order': order,
+ 'products': products,
+ 'total_tax': order.total_tax(),
+ 'total_net': order.net_total(),
+ 'total_gross': order.gross_total(),
+ }
+
+ total_margin = order.total_margin()
+
+ invoice = Invoice(order=order)
+ invoice.created_by = request.user
+ invoice.customer = order.customer
+ invoice.total_margin = total_margin
+
+ if order.customer:
+ customer = order.customer
+ initial['customer_name'] = customer.name
+ initial['customer_phone'] = customer.phone
+ initial['customer_email'] = customer.email
+ initial['customer_address'] = customer.street_address
+ else:
+ initial['customer_name'] = _(u'Walk-In Customer')
+
+ form = InvoiceForm(initial=initial, instance=invoice, prefix='invoice')
+
+ PaymentFormset = inlineformset_factory(Invoice, Payment, extra=1, form=PaymentForm, exclude=[])
+ initial = [{'amount': order.gross_total, 'created_by': request.user}]
+ formset = PaymentFormset(initial=initial, prefix='payment')
+
+ if request.method == 'POST':
+ form = InvoiceForm(request.POST, instance=invoice, prefix='invoice')
+ if form.is_valid():
+ invoice = form.save()
+ formset = PaymentFormset(request.POST, instance=invoice, prefix='payment')
+
+ if formset.is_valid():
+ payments = formset.save()
+ else:
+ messages.error(request, formset.errors)
+ return render(request, "orders/dispatch.html", locals())
+
+ products = request.POST.getlist('items')
+
+ try:
+ order.dispatch(invoice=invoice, products=products)
+ messages.success(request, _(u'Order %s dispatched') % order.code)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+ else:
+ messages.error(request, form.errors)
+
+ return render(request, "orders/dispatch.html", locals())
+
+
+@permission_required('servo.change_order')
+def add_payment(request, pk):
+ invoice = get_object_or_404(Invoice, pk=pk)
+ payment = Payment(invoice=invoice)
+ payment.created_by = request.user
+ payment.amount = request.POST.get('amount')
+ payment.save()
diff --git a/servo/views/note.py b/servo/views/note.py
new file mode 100644
index 0000000..29c74e1
--- /dev/null
+++ b/servo/views/note.py
@@ -0,0 +1,435 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+import StringIO
+from gsxws import escalations
+
+from django import template
+from django.contrib import messages
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+from django.forms.models import modelformset_factory
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.cache import cache_page
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from reportlab.lib.units import mm
+from reportlab.graphics.shapes import Drawing
+from reportlab.graphics.barcode import createBarcodeDrawing
+
+from servo.models import Order, Template, Tag, Customer, Note, Attachment, Escalation
+from servo.forms import NoteForm, NoteSearchForm, EscalationForm
+
+
+class BarcodeDrawing(Drawing):
+ def __init__(self, text_value, *args, **kwargs):
+ barcode = createBarcodeDrawing(
+ "Code128",
+ value=text_value.encode("utf-8"),
+ barHeight=10*mm,
+ width=80*mm
+ )
+
+ Drawing.__init__(self, barcode.width, barcode.height, *args, **kwargs)
+ self.add(barcode, name="barcode")
+
+
+def show_barcode(request, text):
+ """
+ Returns text as a barcode
+ """
+ if request.GET.get('f') == 'svg':
+ import barcode
+ output = StringIO.StringIO()
+ code = barcode.Code39(text, add_checksum=False)
+ code.write(output)
+ contents = output.getvalue()
+ output.close()
+ return HttpResponse(contents, content_type="image/svg+xml")
+
+ d = BarcodeDrawing(text)
+ return HttpResponse(d.asString("png"), content_type="image/png")
+
+
+def prep_list_view(request, kind):
+ """
+ Prepares the view for listing notes/messages
+ """
+ data = {'title': _("Messages")}
+ all_notes = Note.objects.all().order_by("-created_at")
+
+ if kind == "inbox":
+ all_notes = all_notes.filter(order=None).order_by("is_read", "-created_at")
+ if kind == "sent":
+ all_notes = all_notes.filter(created_by=request.user)
+ if kind == "flagged":
+ all_notes = all_notes.filter(is_flagged=True)
+ if kind == "escalations":
+ all_notes = Note.objects.all().exclude(escalation=None)
+
+ page = request.GET.get("page")
+ paginator = Paginator(all_notes, 20)
+
+ try:
+ notes = paginator.page(page)
+ except PageNotAnInteger:
+ notes = paginator.page(1)
+ except EmptyPage:
+ notes = paginator.page(paginator.num_pages)
+
+ data['kind'] = kind
+ data['notes'] = notes
+ data['inbox_count'] = Note.objects.filter(order=None).count()
+
+ return data
+
+
+@permission_required('servo.change_note')
+def copy(request, pk):
+ """
+ Copies a note with its attachments and labels
+ """
+ from servo.lib.shorturl import from_time
+ note = get_object_or_404(Note, pk=pk)
+
+ new_note = Note(created_by=request.user)
+ new_note.body = note.body
+ new_note.order = note.order
+ new_note.subject = note.subject
+ new_note.save()
+
+ new_note.labels = note.labels.all()
+
+ for a in note.attachments.all():
+ a.pk = None
+ a.content_object = new_note
+ a.save()
+ new_note.attachments.add(a)
+
+ return redirect(edit, pk=new_note.pk, order_id=note.order_id)
+
+
+@permission_required('servo.change_note')
+def edit(request, pk=None, order_id=None, parent=None, recipient=None, customer=None):
+ """
+ Edits a note
+ """
+ to = []
+ order = None
+ note = Note(order_id=order_id)
+ excluded_emails = note.get_excluded_emails()
+
+ if recipient is not None:
+ to.append(recipient)
+
+ if order_id is not None:
+ order = get_object_or_404(Order, pk=order_id)
+
+ if order.user and (order.user != request.user):
+ note.is_read = False
+ if order.user.email not in excluded_emails:
+ to.append(order.user.email)
+
+ if order.customer is not None:
+ customer = order.customer_id
+
+ if customer is not None:
+ customer = Customer.objects.get(pk=customer)
+ note.customer = customer
+
+ if order_id is None:
+ to.append(customer.email)
+
+ tpl = template.Template(note.subject)
+ note.subject = tpl.render(template.Context({'note': note}))
+
+ note.recipient = ', '.join(to)
+ note.created_by = request.user
+ note.sender = note.get_default_sender()
+
+ fields = escalations.CONTEXTS
+
+ try:
+ note.escalation = Escalation(created_by=request.user)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(request.META['HTTP_REFERER'])
+
+ AttachmentFormset = modelformset_factory(Attachment,
+ fields=('content',),
+ can_delete=True,
+ extra=3,
+ exclude=[])
+ formset = AttachmentFormset(queryset=Attachment.objects.none())
+
+ if pk is not None:
+ note = get_object_or_404(Note, pk=pk)
+ formset = AttachmentFormset(queryset=note.attachments.all())
+
+ if parent is not None:
+ parent = Note.objects.get(pk=parent)
+ note.parent = parent
+ note.body = parent.quote()
+
+ if parent.subject:
+ note.subject = _(u'Re: %s') % parent.clean_subject()
+ if parent.sender not in excluded_emails:
+ note.recipient = parent.sender
+ if parent.order:
+ order = parent.order
+ note.order = parent.order
+
+ note.customer = parent.customer
+ note.escalation = parent.escalation
+ note.is_reported = parent.is_reported
+
+ title = note.subject
+ form = NoteForm(instance=note)
+
+ if note.escalation:
+ contexts = json.loads(note.escalation.contexts)
+
+ escalation_form = EscalationForm(prefix='escalation', instance=note.escalation)
+
+ if request.method == "POST":
+ escalation_form = EscalationForm(request.POST,
+ prefix='escalation',
+ instance=note.escalation)
+
+ if escalation_form.is_valid():
+ note.escalation = escalation_form.save()
+
+ form = NoteForm(request.POST, instance=note)
+
+ if form.is_valid():
+
+ note = form.save()
+ formset = AttachmentFormset(request.POST, request.FILES)
+
+ if formset.is_valid():
+
+ files = formset.save(commit=False)
+
+ for f in files:
+ f.content_object = note
+ try:
+ f.save()
+ except ValueError, e:
+ messages.error(request, e)
+ return redirect(note)
+
+ note.attachments.add(*files)
+ note.save()
+
+ try:
+ msg = note.send_and_save(request.user)
+ messages.success(request, msg)
+ except ValueError, e:
+ messages.error(request, e)
+
+ return redirect(note)
+
+ return render(request, "notes/form.html", locals())
+
+
+def delete_note(request, pk):
+ """
+ Deletes a note
+ """
+ note = get_object_or_404(Note, pk=pk)
+
+ if request.method == 'POST':
+ note.delete()
+ messages.success(request, _("Note deleted"))
+
+ if request.session.get('return_to'):
+ url = request.session.get('return_to')
+ del(request.session['return_to'])
+ elif note.order_id:
+ url = note.order.get_absolute_url()
+
+ return redirect(url)
+
+ return render(request, 'notes/remove.html', {'note': note})
+
+
+@csrf_exempt
+def render_template(request):
+ """
+ Renders the template with this title with the current
+ Service Order as the context
+ """
+ content = ''
+ title = request.POST.get('title')
+ tpl = Template.objects.get(title=title)
+
+ if request.session.get('current_order_id'):
+ tpl = template.Template(tpl.content)
+ order = Order.objects.get(pk=request.session['current_order_id'])
+ content = tpl.render(template.Context({'order': order}))
+
+ return HttpResponse(content)
+
+
+def templates(request, template_id=None):
+ if template_id is not None:
+ tpl = Template.objects.get(pk=template_id)
+ content = tpl.content
+ if request.session.get('current_order_id'):
+ tpl = template.Template(content)
+ order = Order.objects.get(pk=request.session['current_order_id'])
+ content = tpl.render(template.Context({'order': order}))
+
+ return HttpResponse(content)
+
+ templates = Template.objects.all()
+ return render(request, 'notes/templates.html', {'templates': templates})
+
+
+def toggle_flag(request, pk, flag):
+ field = 'is_%s' % flag
+ note = Note.objects.get(pk=pk)
+ attr = getattr(note, field)
+ setattr(note, field, not attr)
+ note.save()
+
+ return HttpResponse(getattr(note, 'get_%s_title' % flag)())
+
+
+def toggle_tag(request, pk, tag_id):
+ note = Note.objects.get(pk=pk)
+ tag = Tag.objects.get(pk=tag_id)
+
+ if tag in note.labels.all():
+ note.labels.remove(tag)
+ else:
+ note.labels.add(tag)
+
+ if note.order:
+ return redirect(note.order)
+
+ return HttpResponse(_('OK'))
+
+def list_notes(request, kind="inbox"):
+ data = prep_list_view(request, kind)
+ request.session['return_to'] = request.path
+ return render(request, "notes/list_notes.html", data)
+
+
+def view_note(request, kind, pk):
+ note = Note.objects.get(pk=pk)
+ data = prep_list_view(request, kind)
+ data['title'] = note.subject
+ data['note'] = note
+
+ if kind == 'escalations':
+ return render(request, "notes/view_escalation.html", data)
+ else:
+ return render(request, "notes/view_note.html", data)
+
+
+def search(request):
+ query = request.GET.get("q")
+ request.session['search_query'] = query
+
+ title = _(u'Notes containing "%s"') % query
+ results = Note.objects.filter(body__icontains=query).order_by('-created_at')
+ paginator = Paginator(results, 10)
+
+ page = request.GET.get("page")
+
+ try:
+ notes = paginator.page(page)
+ except PageNotAnInteger:
+ notes = paginator.page(1)
+ except EmptyPage:
+ notes = paginator.page(paginator.num_pages)
+
+ return render(request, "notes/search.html", locals())
+
+
+def find(request):
+ form = NoteSearchForm(request.GET)
+ results = Note.objects.none()
+
+ if request.GET and form.is_valid():
+
+ fdata = form.cleaned_data
+ results = Note.objects.all()
+
+ if fdata.get('body'):
+ results = results.filter(body__icontains=fdata['body'])
+ if fdata.get('recipient'):
+ results = results.filter(recipient__icontains=fdata['recipient'])
+ if fdata.get('sender'):
+ results = results.filter(sender__icontains=fdata['sender'])
+ if fdata.get('order_code'):
+ results = results.filter(order__code__icontains=fdata['order_code'])
+
+ results = results.order_by('-created_at')
+
+ paginator = Paginator(results, 10)
+ page = request.GET.get("page")
+
+ try:
+ notes = paginator.page(page)
+ except PageNotAnInteger:
+ notes = paginator.page(1)
+ except EmptyPage:
+ notes = paginator.page(paginator.num_pages)
+
+ title = _('Message search')
+ return render(request, "notes/find.html", locals())
+
+
+def edit_escalation(request):
+ pass
+
+
+def create_escalation(request):
+ esc = Escalation()
+ form = EscalationForm()
+ title = _('Edit Escalation')
+
+ if request.method == 'POST':
+ data = request.POST.copy()
+ data['created_by'] = request.user
+ form = EscalationForm(data, request.FILES, instance=esc)
+ if form.is_valid():
+ note = form.save()
+ #esc.submit(request.user)
+ return redirect(view_note, 'escalations', note.pk)
+
+ return render(request, 'notes/edit_escalation.html', locals())
+
+
+def list_messages(request, pk):
+ note = get_object_or_404(Note, pk=pk)
+ messages = note.message_set.all()
+ return render(request, "notes/messages.html", locals())
diff --git a/servo/views/order.py b/servo/views/order.py
new file mode 100644
index 0000000..00767ab
--- /dev/null
+++ b/servo/views/order.py
@@ -0,0 +1,990 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+
+from gsxws.core import GsxError
+from django.http import QueryDict
+
+from django.db.models import Q
+from django.utils import timezone
+from django.contrib import messages
+from django.core.cache import cache
+from django.http import HttpResponse
+
+from django.db import DatabaseError
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+from django.views.decorators.cache import cache_page
+from django.shortcuts import render, redirect, get_object_or_404
+
+from django.views.decorators.csrf import csrf_exempt
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models.order import *
+from servo.forms.orders import *
+
+from servo.models import Note, User, Device, Customer
+from servo.models.common import (Tag,
+ Configuration,
+ FlaggedItem,
+ GsxAccount,)
+from servo.models.repair import (Checklist,
+ ChecklistItem,
+ Repair,
+ ChecklistItemValue,)
+
+
+def prepare_list_view(request, args):
+ """
+ Lists service orders matching specified criteria
+ """
+ data = {'title': _("Orders")}
+
+ form = OrderSearchForm(args)
+ form.fields['location'].queryset = request.user.locations
+
+ if request.session.get("current_queue"):
+ del(request.session['current_queue'])
+
+ if request.session.get("return_to"):
+ del(request.session['return_to'])
+
+ if request.user.customer:
+ orders = Order.objects.filter(customer=request.user.customer)
+ else:
+ orders = Order.objects.filter(location__in=request.user.locations.all())
+
+ if args.get("state"):
+ orders = orders.filter(state__in=args.getlist("state"))
+
+ start_date = args.get("start_date")
+ if start_date:
+ end_date = args.get('end_date') or timezone.now()
+ orders = orders.filter(created_at__range=[start_date, end_date])
+
+ if args.get("assigned_to"):
+ users = args.getlist("assigned_to")
+ orders = orders.filter(user__in=users)
+
+ if args.get("followed_by"):
+ users = args.getlist("followed_by")
+ orders = orders.filter(followed_by__in=users)
+
+ if args.get("created_by"):
+ users = args.getlist("created_by")
+ orders = orders.filter(created_by__in=users)
+
+ if args.get("customer"):
+ customer = int(args['customer'][0])
+ if customer == 0:
+ orders = orders.filter(customer__pk=None)
+ else:
+ orders = orders.filter(customer__tree_id=customer)
+
+ if args.get("spec"):
+ spec = args['spec'][0]
+ if spec is "None":
+ orders = orders.filter(devices=None)
+ else:
+ orders = orders.filter(devices__slug=spec)
+
+ if args.get("device"):
+ orders = orders.filter(devices__pk=args['device'])
+
+ if args.get("queue"):
+ queue = args.getlist("queue")
+ orders = orders.filter(queue__in=queue)
+
+ if args.get("checkin_location"):
+ ci_location = args.getlist("checkin_location")
+ orders = orders.filter(checkin_location__in=ci_location)
+
+ if args.get("location"):
+ location = args.getlist("location")
+ orders = orders.filter(location__in=location)
+
+ if args.get("label"):
+ orders = orders.filter(tags__in=args.getlist("label"))
+
+ if args.get("status"):
+ status = args.getlist("status")
+
+ if args['status'][0] == 'None':
+ orders = orders.filter(status__pk=None)
+ else:
+ orders = orders.filter(status__status__in=status)
+
+ if args.get("color"):
+ color = args.getlist("color")
+ now = timezone.now()
+
+ if "grey" in color:
+ orders = orders.filter(status=None)
+ if "green" in color:
+ orders = orders.filter(status_limit_green__gte=now)
+ if "yellow" in color:
+ orders = orders.filter(status_limit_yellow__gte=now,
+ status_limit_green__lte=now)
+ if "red" in color:
+ orders = orders.filter(status_limit_yellow__lte=now)
+
+ page = request.GET.get("page")
+ paginator = Paginator(orders.distinct(), 100)
+
+ try:
+ order_pages = paginator.page(page)
+ except PageNotAnInteger:
+ order_pages = paginator.page(1)
+ except EmptyPage:
+ order_pages = paginator.page(paginator.num_pages)
+
+ data['form'] = form
+ data['queryset'] = orders
+ data['orders'] = order_pages
+ data['subtitle'] = _("%d search results") % orders.count()
+
+ # @FIXME!!! how to handle this with jsonserializer???
+ #request.session['order_queryset'] = orders
+
+ return data
+
+
+def prepare_detail_view(request, pk):
+ """
+ Prepares the view for whenever we're dealing with a specific order
+ """
+ order = get_object_or_404(Order, pk=pk)
+
+ request.session['current_order_id'] = None
+ request.session['current_order_code'] = None
+ request.session['current_order_customer'] = None
+
+ title = _(u'Order %s') % order.code
+ priorities = Queue.PRIORITIES
+ followers = order.followed_by.all()
+ locations = Location.objects.filter(enabled=True)
+ queues = request.user.queues.all()
+ users = order.get_available_users(request.user)
+
+ # wrap the customer in a list for easier recursetree
+ if order.customer is not None:
+ customer = order.customer.get_ancestors(include_self=True)
+ title = u'%s | %s' % (title, order.customer.name)
+ else:
+ customer = []
+
+ statuses = []
+ checklists = []
+
+ if order.queue is not None:
+ checklists = Checklist.objects.filter(queues=order.queue)
+ statuses = order.queue.queuestatus_set.all()
+
+ if order.is_editable:
+ request.session['current_order_id'] = order.pk
+ request.session['current_order_code'] = order.code
+ request.session['return_to'] = order.get_absolute_url()
+ if order.customer:
+ request.session['current_order_customer'] = order.customer.pk
+
+ return locals()
+
+
+@permission_required("servo.change_order")
+def close(request, pk):
+ """
+ Closes this Service Order
+ """
+ order = Order.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ order.close(request.user)
+ except Exception as e:
+ messages.error(request, e)
+ return redirect(order)
+
+ if request.session.get("current_order_id"):
+ del(request.session['current_order_id'])
+ del(request.session['current_order_code'])
+ del(request.session['current_order_customer'])
+
+ messages.success(request, _('Order %s closed') % order.code)
+
+ return redirect(order)
+
+ data = {'order': order, 'action': request.path}
+ return render(request, "orders/close.html", data)
+
+
+@permission_required("servo.delete_order")
+def reopen_order(request, pk):
+ order = Order.objects.get(pk=pk)
+ msg = order.reopen(request.user)
+ messages.success(request, msg)
+ return redirect(order)
+
+
+@permission_required("servo.add_order")
+def create(request, sn=None, device_id=None, product_id=None, note_id=None, customer_id=None):
+ """
+ Creates a new Service Order
+ """
+ order = Order(created_by=request.user)
+
+ if customer_id is not None:
+ order.customer_id = customer_id
+
+ try:
+ order.save()
+ except Exception as e:
+ messages.error(request, e)
+ return redirect(list_orders)
+
+ messages.success(request, _("Order %s created") % order.code)
+
+ # create service order from a new device
+ if sn is not None:
+ order.add_device_sn(sn, request.user)
+
+ if device_id is not None:
+ device = Device.objects.get(pk=device_id)
+ order.add_device(device, request.user)
+
+ # creating an order from a product
+ if product_id is not None:
+ return redirect(add_product, order.pk, product_id)
+
+ # creating an order from a note
+ if note_id is not None:
+ note = Note.objects.get(pk=note_id)
+ note.order = order
+ note.save()
+ # try to match a customer
+ if note.sender:
+ try:
+ customer = Customer.objects.get(email=note.sender)
+ order.customer = customer
+ order.save()
+ except Customer.DoesNotExist:
+ pass
+
+ return redirect(order)
+
+
+def list_orders(request):
+ """
+ orders/index
+ """
+ args = request.GET.copy()
+ default = QueryDict('state={0}'.format(Order.STATE_QUEUED))
+
+ if len(args) < 2: # search form not submitted
+ args = request.session.get("order_search_filter", default)
+
+ request.session['order_search_filter'] = args
+ data = prepare_list_view(request, args)
+
+ return render(request, "orders/index.html", data)
+
+
+@permission_required("servo.change_order")
+def toggle_tag(request, order_id, tag_id):
+ tag = Tag.objects.get(pk=tag_id)
+ order = Order.objects.get(pk=order_id)
+
+ if tag not in order.tags.all():
+ order.add_tag(tag)
+ else:
+ order.tags.remove(tag)
+
+ return HttpResponse(tag.title)
+
+
+@permission_required("servo.change_order")
+def toggle_task(request, order_id, item_id):
+ """
+ Toggles a given Check List item in this order
+ """
+ checklist_item = ChecklistItem.objects.get(pk=item_id)
+
+ try:
+ item = ChecklistItemValue.objects.get(order_id=order_id, item=checklist_item)
+ item.delete()
+ except ChecklistItemValue.DoesNotExist:
+ item = ChecklistItemValue()
+ item.item = checklist_item
+ item.order_id = order_id
+ item.checked_by = request.user
+ item.save()
+
+ return HttpResponse(checklist_item.title)
+
+
+def repair(request, order_id, repair_id):
+ """
+ Show the corresponding GSX Repair for this Service Order
+ """
+ repair = get_object_or_404(Repair, pk=repair_id)
+ data = prepare_detail_view(request, order_id)
+ data['repair'] = repair
+
+ try:
+ repair.connect_gsx(request.user)
+ details = repair.get_details()
+ try:
+ data['notes'] = details.notes.encode('utf-8')
+ except AttributeError:
+ pass
+ data['status'] = repair.update_status(request.user)
+ except Exception as e:
+ messages.error(request, e)
+
+ data['parts'] = repair.servicepart_set.all()
+ return render(request, "orders/repair.html", data)
+
+
+@permission_required("servo.change_order")
+def complete_repair(request, order_id, repair_id):
+ repair = Repair.objects.get(pk=repair_id)
+ if request.method == 'POST':
+ try:
+ repair.close(request.user)
+ msg = _(u"Repair %s marked complete.") % repair.confirmation
+ messages.success(request, msg)
+ except GsxError, e:
+ messages.error(request, e)
+
+ return redirect(repair.order)
+
+ return render(request, 'orders/close_repair.html', locals())
+
+
+@csrf_exempt
+@permission_required("servo.change_order")
+def accessories(request, pk, device_id):
+ from django.utils import safestring
+
+ if request.POST.get('name'):
+ a = Accessory(name=request.POST['name'])
+ a.order_id = pk
+ a.device_id = device_id
+ a.save()
+
+ choice_list = []
+ choices = Accessory.objects.distinct('name')
+
+ for c in choices:
+ choice_list.append(c.name)
+
+ action = reverse('orders-accessories', args=[pk, device_id])
+ selected = Accessory.objects.filter(order_id=pk, device_id=device_id)
+ choices_json = safestring.mark_safe(json.dumps(choice_list))
+
+ return render(request, 'devices/accessories_edit.html', locals())
+
+
+@permission_required('servo.change_order')
+def delete_accessory(request, order_id, device_id, pk):
+ Accessory.objects.filter(pk=pk).delete()
+ return accessories(request, order_id, device_id)
+
+
+@permission_required("servo.change_order")
+def edit(request, pk):
+ data = prepare_detail_view(request, pk)
+ data['note_tags'] = Tag.objects.filter(type='note')
+ return render(request, "orders/edit.html", data)
+
+
+@permission_required('servo.delete_order')
+def delete(request, pk):
+
+ order = get_object_or_404(Order, pk=pk)
+
+ if request.method == "POST":
+ return_to = order.get_queue_url()
+ try:
+ order.delete()
+ del(request.session['current_order_id'])
+ del(request.session['current_order_code'])
+ del(request.session['current_order_customer'])
+ messages.success(request, _(u'Order %s deleted') % order.code)
+ return redirect(return_to)
+ except Exception as e:
+ ed = {'order': order.code, 'error': e}
+ messages.error(request, _(u'Cannot delete order %(order)s: %(error)s') % ed)
+ return redirect(order)
+
+ action = request.path
+ return render(request, "orders/delete_order.html", locals())
+
+
+@permission_required('servo.change_order')
+def toggle_follow(request, order_id):
+ order = Order.objects.get(pk=order_id)
+ data = {'icon': "open", 'action': _("Follow")}
+
+ if request.user in order.followed_by.all():
+ order.followed_by.remove(request.user)
+ else:
+ order.followed_by.add(request.user)
+ data = {'icon': "close", 'action': _("Unfollow")}
+
+ if request.is_ajax():
+ return render(request, "orders/toggle_follow.html", data)
+
+ return redirect(order)
+
+
+def toggle_flagged(request, pk):
+ order = Order.objects.get(pk=pk)
+ t = FlaggedItem(content_object=order, flagged_by=request.user)
+ t.save()
+
+
+@permission_required("servo.change_order")
+def remove_user(request, pk, user_id):
+ """
+ Removes this user from the follower list, unsets assignee
+ """
+ order = get_object_or_404(Order, pk=pk)
+ user = User.objects.get(pk=user_id)
+
+ try:
+ order.remove_follower(user)
+ if user == order.user:
+ order.set_user(None, request.user)
+ order.notify("unset_user", _('User %s removed from followers') % user, request.user)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def update_order(request, pk, what, what_id):
+ """
+ Updates some things about an order
+ """
+ order = get_object_or_404(Order, pk=pk)
+ what_id = int(what_id)
+
+ if order.state is Order.STATE_CLOSED:
+ messages.error(request, _("Closed orders cannot be modified"))
+ return redirect(order)
+
+ if what == "user":
+ if request.method == "POST":
+ fullname = request.POST.get("user")
+ try:
+ user = User.active.get(full_name=fullname)
+ if order.user is None:
+ order.set_user(user, request.user)
+ else:
+ order.add_follower(user)
+ order.save()
+ except User.DoesNotExist:
+ messages.error(request, _(u"User %s not found") % fullname)
+ elif what_id > 0:
+ user = User.objects.get(pk=what_id)
+ order.set_user(user, request.user)
+
+ if what == "queue":
+ order.set_queue(what_id, request.user)
+
+ if what == "status":
+ order.set_status(what_id, request.user)
+
+ if what == "priority":
+ order.priority = what_id
+ order.save()
+
+ if what == "place" and request.method == "POST":
+ place = request.POST.get("place")
+ order.notify("set_place", place, request.user)
+ order.place = place
+ order.save()
+
+ if what == "label" and request.method == "POST":
+ label = request.POST.get("label")
+
+ try:
+ tag = Tag.objects.get(title=label, type="order")
+ order.add_tag(tag, request.user)
+ except Tag.DoesNotExist:
+ messages.error(request, _(u"Label %s does not exist") % label)
+
+ if what == "checkin":
+ location = Location.objects.get(pk=what_id)
+ order.checkin_location = location
+ messages.success(request, _('Order updated'))
+ order.save()
+
+ if what == "checkout":
+ location = Location.objects.get(pk=what_id)
+ order.checkout_location = location
+ messages.success(request, _('Order updated'))
+ order.save()
+
+ if what == "location":
+ location = Location.objects.get(pk=what_id)
+ msg = order.set_location(location, request.user)
+ messages.success(request, msg)
+
+ request.session['current_order_id'] = order.pk
+ request.session['current_order_code'] = order.code
+ if order.queue:
+ request.session['current_order_queue'] = order.queue.pk
+
+ return redirect(order)
+
+
+def put_on_paper(request, pk, kind="confirmation"):
+ """
+ 'Print' was taken?
+ """
+ conf = Configuration.conf()
+ order = get_object_or_404(Order, pk=pk)
+
+ title = _(u"Service Order #%s") % order.code
+ notes = order.note_set.filter(is_reported=True)
+
+ template = order.get_print_template(kind)
+
+ if kind == "receipt":
+ try:
+ invoice = order.invoice_set.latest()
+ except Exception as e:
+ pass
+
+ return render(request, template, locals())
+
+
+@permission_required("servo.change_order")
+def add_device(request, pk, device_id=None, sn=None):
+ """
+ Adds a device to a service order
+ using device_id with existing devices or
+ sn for new devices (which should have gone through GSX search)
+ """
+ order = get_object_or_404(Order, pk=pk)
+
+ if device_id is not None:
+ device = Device.objects.get(pk=device_id)
+
+ if sn is not None:
+ sn = sn.upper()
+ # not using get() since SNs are not unique
+ device = Device.objects.filter(sn=sn).first()
+
+ if device is None:
+ try:
+ device = Device.from_gsx(sn)
+ device.save()
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+
+ try:
+ event = order.add_device(device, request.user)
+ messages.success(request, event)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+
+ if order.customer:
+ order.customer.devices.add(device)
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def remove_device(request, order_id, device_id):
+ action = request.path
+ order = Order.objects.get(pk=order_id)
+ device = Device.objects.get(pk=device_id)
+
+ if request.method == "POST":
+ msg = order.remove_device(device, request.user)
+ messages.info(request, msg)
+ return redirect(order)
+
+ return render(request, "orders/remove_device.html", locals())
+
+
+def events(request, order_id):
+ data = prepare_detail_view(request, order_id)
+ return render(request, "orders/events.html", data)
+
+
+def device_from_product(request, pk, item_id):
+ """
+ Turns a SOI into a device and attaches it to this order
+ """
+ order = Order.objects.get(pk=pk)
+ soi = ServiceOrderItem.objects.get(pk=item_id)
+
+ try:
+ GsxAccount.default(request.user, order.queue)
+ device = Device.from_gsx(soi.sn)
+ device.save()
+ event = order.add_device(device, request.user)
+ messages.success(request, event)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(order)
+
+
+@permission_required('servo.change_order')
+def reserve_products(request, pk):
+ order = Order.objects.get(pk=pk)
+ location = request.user.get_location()
+
+ if request.method == 'POST':
+
+ for p in order.serviceorderitem_set.all():
+ p.reserve_product()
+
+ msg = _(u"Products of order %s reserved") % order.code
+ order.notify("products_reserved", msg, request.user)
+ messages.info(request, msg)
+
+ return redirect(order)
+
+ data = {'order': order, 'action': request.path}
+ return render(request, "orders/reserve_products.html", data)
+
+
+@permission_required("servo.change_order")
+def edit_product(request, pk, item_id):
+ """
+ Edits a product added to an order
+ """
+ order = Order.objects.get(pk=pk)
+ item = ServiceOrderItem.objects.get(pk=item_id)
+
+ if not item.kbb_sn and item.product.part_type == "REPLACEMENT":
+ try:
+ device = order.devices.all()[0]
+ item.kbb_sn = device.sn
+ except IndexError:
+ pass # Probably no device in the order
+
+ if item.product.component_code:
+ try:
+ GsxAccount.default(request.user, order.queue)
+ except Exception, e:
+ return render(request, "snippets/error_modal.html", {'error': e})
+
+ form = OrderItemForm(instance=item)
+
+ if request.method == "POST":
+ form = OrderItemForm(request.POST, instance=item)
+ if form.is_valid():
+ try:
+ item = form.save()
+ # Set whoever set the KBB sn as the one who replaced the part
+ if item.kbb_sn and not item.replaced_by:
+ item.replaced_by = request.user
+ item.save()
+
+ messages.success(request, _(u"Product %s saved") % item.code)
+
+ return redirect(order)
+ except Exception, e:
+ messages.error(request, e)
+
+ product = item.product
+ title = product.code
+ prices = json.dumps(item.product.get_price())
+
+ return render(request, "orders/edit_product.html", locals())
+
+
+@permission_required("servo.change_order")
+def add_product(request, pk, product_id):
+ "Adds this product to this Sales Order"
+ order = Order.objects.get(pk=pk)
+ product = Product.objects.get(pk=product_id)
+ order.add_product(product, 1, request.user)
+ messages.success(request, _(u'Product %s added') % product.code)
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def add_part(request, pk, device, code):
+ """
+ Adds a part for this device to this order
+ """
+ gsx_product = cache.get(code)
+ order = Order.objects.get(pk=pk)
+ device = Device.objects.get(pk=device)
+
+ try:
+ product = Product.objects.get(code=code)
+ if not product.fixed_price:
+ product.update_price(gsx_product)
+ except Product.DoesNotExist:
+ product = gsx_product
+
+ product.save()
+
+ try:
+ tag, created = TaggedItem.objects.get_or_create(
+ content_type__model="product",
+ object_id=product.pk,
+ tag=device.description
+ )
+ tag.save()
+ except DatabaseError:
+ pass
+
+ order.add_product(product, 1, request.user)
+
+ return render(request, "orders/list_products.html", locals())
+
+
+def choose_product(request, order_id):
+ pass
+
+
+@permission_required("servo.change_order")
+def report_product(request, pk, item_id):
+ product = ServiceOrderItem.objects.get(pk=item_id)
+ product.should_report = not product.should_report
+ product.save()
+
+ if product.should_report:
+ return HttpResponse('<i class="icon-ok"></i>')
+
+ return HttpResponse('<i class="icon-ok icon-white"></i>')
+
+
+@permission_required("servo.change_order")
+def report_device(request, pk, device_id):
+ device = OrderDevice.objects.get(pk=device_id)
+ device.should_report = not device.should_report
+ device.save()
+
+ if device.should_report:
+ return HttpResponse('<i class="icon-ok"></i>')
+
+ return HttpResponse('<i class="icon-ok icon-white"></i>')
+
+
+@permission_required('servo.change_order')
+def remove_product(request, pk, item_id):
+ order = Order.objects.get(pk=pk)
+
+ # The following is to help those who hit Back after removing a product
+ try:
+ item = ServiceOrderItem.objects.get(pk=item_id)
+ except ServiceOrderItem.DoesNotExist:
+ messages.error(request, _("Order item does not exist"))
+ return redirect(order)
+
+ if request.method == 'POST':
+ msg = order.remove_product(item, request.user)
+ messages.info(request, msg)
+ return redirect(order)
+
+ return render(request, 'orders/remove_product.html', locals())
+
+
+@permission_required('servo.change_order')
+def products(request, pk, item_id=None, action='list'):
+ order = Order.objects.get(pk=pk)
+ if action == 'list':
+ return render(request, 'orders/products.html', {'order': order})
+
+
+@permission_required('servo.change_order')
+def list_products(request, pk):
+ order = Order.objects.get(pk=pk)
+ return render(request, "orders/list_products.html", locals())
+
+
+def parts(request, order_id, device_id, queue_id):
+ """
+ Selects parts for this device in this order
+ """
+ order = Order.objects.get(pk=order_id)
+ device = Device.objects.get(pk=device_id)
+ title = device.description
+ url = reverse('devices-parts', args=[device_id, order_id, queue_id])
+
+ if order.queue is not None:
+ request.session['current_queue'] = order.queue.pk
+
+ return render(request, "orders/parts.html", locals())
+
+
+@permission_required("servo.change_order")
+def select_customer(request, pk, customer_id):
+ """
+ Selects a specific customer for this order
+ """
+ order = Order.objects.get(pk=pk)
+ order.customer_id = customer_id
+ order.save()
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def choose_customer(request, pk):
+ """
+ Lets the user search for a customer for this order
+ """
+ if request.method == "POST":
+ customers = Customer.objects.none()
+ kind = request.POST.get('kind')
+ query = request.POST.get('name')
+
+ if len(query) > 2:
+ customers = Customer.objects.filter(
+ Q(fullname__icontains=query)
+ | Q(email__icontains=query)
+ | Q(phone__contains=query)
+ )
+
+ if kind == 'companies':
+ customers = customers.filter(is_company=True)
+
+ if kind == 'contacts':
+ customers = customers.filter(is_company=False)
+
+ data = {'customers': customers, 'order_id': pk}
+ return render(request, "customers/choose-list.html", data)
+
+ data = {'action': request.path}
+ return render(request, 'customers/choose.html', data)
+
+
+@permission_required("servo.change_order")
+def remove_customer(request, pk, customer_id):
+ if request.method == "POST":
+ order = Order.objects.get(pk=pk)
+ customer = Customer.objects.get(pk=customer_id)
+ order.customer = None
+ order.save()
+ msg = _(u"Customer %s removed") % customer.name
+ order.notify("customer_removed", msg, request.user)
+ messages.success(request, msg)
+ return redirect(order)
+
+ data = {'action': request.path}
+ return render(request, "orders/remove_customer.html", data)
+
+
+def search(request):
+ query = request.GET.get("q")
+
+ if not query or len(query) < 3:
+ messages.error(request, _('Search query is too short'))
+ return redirect(list_orders)
+
+ request.session['search_query'] = query
+
+ # Redirect Order ID:s to the order
+ try:
+ order = Order.objects.get(code__iexact=query)
+ return redirect(order)
+ except Order.DoesNotExist:
+ pass
+
+ orders = Order.objects.filter(
+ Q(code=query) | Q(devices__sn__contains=query) |
+ Q(customer__fullname__icontains=query) |
+ Q(customer__phone__contains=query) |
+ Q(repair__confirmation=query) |
+ Q(repair__reference=query)
+ )
+
+ data = {'title': _(u'Search results for "%s"') % query}
+ data['orders'] = orders.distinct()
+
+ return render(request, "orders/index.html", data)
+
+
+@permission_required("servo.add_order")
+def copy_order(request, pk):
+ order = Order.objects.get(pk=pk)
+ new_order = order.duplicate(request.user)
+ return redirect(new_order)
+
+
+def history(request, pk, device):
+ device = get_object_or_404(Device, pk=device)
+ orders = device.order_set.exclude(pk=pk)
+ return render(request, "orders/history.html", locals())
+
+
+@permission_required("servo.batch_process")
+def batch_process(request):
+ form = BatchProcessForm()
+ title = _('Batch Processing')
+
+ if request.method == 'POST':
+ form = BatchProcessForm(request.POST)
+ if form.is_valid():
+ from servo.tasks import batch_process
+ batch_process.delay(request.user, form.cleaned_data)
+ messages.success(request, _('Request accepted for batch processing'))
+
+ return render(request, "orders/batch_process.html", locals())
+
+
+def download_results(request):
+ import csv
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename="orders.csv"'
+
+ writer = csv.writer(response)
+ header = [
+ 'CODE',
+ 'CUSTOMER',
+ 'CREATED_AT',
+ 'ASSIGNED_TO',
+ 'CHECKED_IN',
+ 'LOCATION'
+ ]
+ writer.writerow(header)
+
+ for o in request.session['order_queryset']:
+ row = [o.code, o.customer, o.created_at,
+ o.user, o.checkin_location, o.location]
+ coded = [unicode(s).encode('utf-8') for s in row]
+
+ writer.writerow(coded)
+
+ return response
diff --git a/servo/views/product.py b/servo/views/product.py
new file mode 100644
index 0000000..3dd2202
--- /dev/null
+++ b/servo/views/product.py
@@ -0,0 +1,474 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+from decimal import *
+
+from django.db.models import Q
+from django.db import IntegrityError
+
+from django.contrib import messages
+from django.core.cache import cache
+from django.http import HttpResponse
+from django.shortcuts import render, redirect
+from django.utils.translation import ugettext as _
+from django.forms.models import inlineformset_factory
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.auth.decorators import permission_required
+
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models import (Attachment, TaggedItem,
+ Product, ProductCategory,
+ Inventory, Location, inventory_totals,
+ GsxAccount)
+from servo.forms.product import ProductForm, CategoryForm, ProductSearchForm
+
+
+def prep_list_view(request, group='all'):
+ """
+ Prepares the product list view
+ """
+ title = _("Products")
+ all_products = Product.objects.all()
+ categories = ProductCategory.objects.all()
+
+ if group == 'all':
+ group = ProductCategory(title=_('All'), slug='all')
+ else:
+ group = categories.get(slug=group)
+ all_products = group.get_products()
+
+ if request.method == 'POST':
+ form = ProductSearchForm(request.POST)
+ if form.is_valid():
+ fdata = form.cleaned_data
+
+ description = fdata.get('description')
+ if description:
+ all_products = all_products.filter(description__icontains=description)
+
+ title = fdata.get('title')
+ if title:
+ all_products = all_products.filter(title__icontains=title)
+
+ code = fdata.get('code')
+ if code:
+ all_products = all_products.filter(code__icontains=code)
+
+ tag = fdata.get('tag')
+ if tag:
+ tag = tag.tag
+ title += u" / %s" % tag
+ all_products = all_products.filter(tags__tag=tag)
+ else:
+ form = ProductSearchForm()
+
+ title += u" / %s" % group.title
+ page = request.GET.get("page")
+ paginator = Paginator(all_products.distinct(), 25)
+
+ try:
+ products = paginator.page(page)
+ except PageNotAnInteger:
+ products = paginator.page(1)
+ except EmptyPage:
+ products = paginator.page(paginator.num_pages)
+
+ return locals()
+
+
+def tags(request):
+ """
+ Returns all product tags
+ """
+ tags = TaggedItem.objects.filter(content_type__model="product")
+ tags = tags.distinct("tag").values_list("tag", flat=True)
+ return HttpResponse(json.dumps(list(tags)), content_type='application/json')
+
+
+def list_products(request, group='all'):
+ data = prep_list_view(request, group)
+ p, s = inventory_totals()
+ data['total_sales_value'] = s
+ data['total_purchase_value'] = p
+
+ return render(request, "products/index.html", data)
+
+
+@permission_required("servo.change_product")
+def upload_gsx_parts(request, group=None):
+ from servo.forms.product import PartsImportForm
+ form = PartsImportForm()
+
+ data = {'action': request.path}
+
+ if request.method == "POST":
+
+ form = PartsImportForm(request.POST, request.FILES)
+
+ if form.is_valid():
+ data = form.cleaned_data
+ filename = "servo/uploads/products/partsdb.csv"
+ destination = open(filename, "wb+")
+
+ for chunk in data['partsdb'].chunks():
+ destination.write(chunk)
+
+ messages.success(request, _("Parts database uploaded for processing"))
+ return redirect(list_products)
+
+ data['form'] = form
+ return render(request, "products/upload_gsx_parts.html", data)
+
+
+@permission_required("servo.change_product")
+def download_products(request, group="all"):
+ filename = "products"
+
+ if group == "all":
+ products = Product.objects.all()
+ else:
+ category = ProductCategory.objects.get(slug=group)
+ products = category.get_products()
+ filename = group
+
+ response = HttpResponse(content_type="text/plain; charset=utf-8")
+ response['Content-Disposition'] = 'attachment; filename="%s.txt"' % filename
+
+ response.write(u"ID\tCODE\tTITLE\tPURCHASE_PRICE\tSALES_PRICE\tSTOCKED\n")
+
+ for p in products:
+ row = u"%s\t%s\t%s\t%s\t%s\t%s\n" % (p.pk,
+ p.code,
+ p.title,
+ p.price_purchase_stock,
+ p.price_sales_stock, 0)
+ response.write(row)
+
+ return response
+
+
+@permission_required("servo.change_product")
+def upload_products(request, group=None):
+ """"
+ Format should be the same as from download_products
+ """
+ import io
+ from servo.forms import ProductUploadForm
+ location = request.user.get_location()
+ form = ProductUploadForm()
+
+ if request.method == "POST":
+ form = ProductUploadForm(request.POST, request.FILES)
+
+ if form.is_valid():
+ string = u''
+ category = form.cleaned_data['category']
+ data = form.cleaned_data['datafile'].read()
+
+ for i in ('utf-8', 'latin-1',):
+ try:
+ string = data.decode(i)
+ except:
+ pass
+
+ if not string:
+ raise ValueError(_('Unsupported file encoding'))
+
+ i = 0
+ sio = io.StringIO(string, newline=None)
+
+ for l in sio.readlines():
+ cols = l.strip().split("\t")
+
+ if cols[0] == "ID":
+ continue # Skip header row
+
+ if len(cols) < 2:
+ continue # Skip empty rows
+
+ if len(cols) < 6: # No ID row, pad it
+ cols.insert(0, "")
+
+ product, created = Product.objects.get_or_create(code=cols[1])
+
+ product.title = cols[2].strip(' "').replace('""', '"') # Remove Excel escapes
+ product.price_purchase_stock = cols[3].replace(',', '.')
+ product.price_sales_stock = cols[4].replace(',', '.')
+ product.save()
+
+ if category:
+ product.categories.add(category)
+
+ inventory, created = Inventory.objects.get_or_create(
+ product=product, location=location
+ )
+ inventory.amount_stocked = cols[5]
+ inventory.save()
+ i += 1
+
+ messages.success(request, _(u"%d products imported") % i)
+
+ return redirect(list_products)
+
+ action = request.path
+ title = _("Upload products")
+ return render(request, "products/upload_products.html", locals())
+
+
+@permission_required("servo.change_product")
+def edit_product(request, pk=None, code=None, group='all'):
+
+ initial = {}
+ product = Product()
+
+ data = prep_list_view(request, group)
+
+ if pk is not None:
+ product = Product.objects.get(pk=pk)
+ form = ProductForm(instance=product)
+
+ if not group == 'all':
+ cat = ProductCategory.objects.get(slug=group)
+ initial = {'categories': [cat]}
+ data['group'] = cat
+
+ product.update_photo()
+
+ if code is not None:
+ product = cache.get(code)
+
+ form = ProductForm(instance=product, initial=initial)
+ InventoryFormset = inlineformset_factory(
+ Product,
+ Inventory,
+ extra=1,
+ max_num=1,
+ exclude=[]
+ )
+
+ formset = InventoryFormset(
+ instance=product,
+ initial=[{'location': request.user.location}]
+ )
+
+ if request.method == "POST":
+
+ form = ProductForm(request.POST, request.FILES, instance=product)
+
+ if form.is_valid():
+
+ product = form.save()
+ content_type = ContentType.objects.get(model="product")
+
+ for a in request.POST.getlist("attachments"):
+ doc = Attachment.objects.get(pk=a)
+ product.attachments.add(doc)
+
+ tags = [x for x in request.POST.getlist('tag') if x != '']
+
+ for t in tags:
+ tag, created = TaggedItem.objects.get_or_create(
+ content_type=content_type,
+ object_id=product.pk,
+ tag=t)
+ tag.save()
+
+ formset = InventoryFormset(request.POST, instance=product)
+
+ if formset.is_valid():
+ formset.save()
+ messages.success(request, _(u"Product %s saved") % product.code)
+ return redirect(product)
+ else:
+ messages.error(request, _('Error in inventory details'))
+ else:
+ messages.error(request, _('Error in product info'))
+
+ data['form'] = form
+ data['product'] = product
+ data['formset'] = formset
+ data['title'] = product.title
+
+ return render(request, "products/form.html", data)
+
+
+@permission_required("servo.delete_product")
+def delete_product(request, pk, group):
+ from django.db.models import ProtectedError
+
+ product = Product.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ product.delete()
+ Inventory.objects.filter(product=product).delete()
+ messages.success(request, _("Product deleted"))
+ except ProtectedError:
+ messages.error(request, _('Cannot delete product'))
+
+ return redirect(list_products, group)
+
+ action = request.path
+ return render(request, 'products/remove.html', locals())
+
+
+def search(request):
+
+ query = request.GET.get("q")
+ request.session['search_query'] = query
+
+ results = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query) | Q(eee_code__icontains=query)
+ )
+
+ paginator = Paginator(results, 100)
+ page = request.GET.get("page")
+
+ try:
+ products = paginator.page(page)
+ except PageNotAnInteger:
+ products = paginator.page(1)
+ except EmptyPage:
+ products = paginator.page(paginator.num_pages)
+
+ title = _(u'Search results for "%s"') % query
+ group = ProductCategory(title=_('All'), slug='all')
+
+ return render(request, 'products/search.html', locals())
+
+
+def view_product(request, pk=None, code=None, group=None):
+
+ product = Product()
+ inventory = Inventory.objects.none()
+
+ try:
+ product = Product.objects.get(pk=pk)
+ inventory = Inventory.objects.filter(product=product)
+ except Product.DoesNotExist:
+ product = cache.get(code)
+
+ data = prep_list_view(request, group)
+
+ data['product'] = product
+ data['title'] = product.title
+ data['inventory'] = inventory
+
+ return render(request, "products/view.html", data)
+
+
+@permission_required("servo.change_productcategory")
+def edit_category(request, slug=None, parent_slug=None):
+
+ form = CategoryForm()
+ category = ProductCategory()
+
+ if slug is not None:
+ category = ProductCategory.objects.get(slug=slug)
+ form = CategoryForm(instance=category)
+
+ if parent_slug is not None:
+ parent = ProductCategory.objects.get(slug=parent_slug)
+ form = CategoryForm(initial={'parent': parent.pk})
+
+ if request.method == "POST":
+ form = CategoryForm(request.POST, instance=category)
+ if form.is_valid():
+ try:
+ category = form.save()
+ except IntegrityError:
+ messages.error(request, _(u'Category %s already exists') % category.title)
+ return redirect(list_products)
+ messages.success(request, _(u"Category %s saved") % category.title)
+ return redirect(category)
+ else:
+ messages.error(request, form.errors)
+ return redirect(list_products)
+
+ return render(request, "products/category_form.html", locals())
+
+
+@permission_required("servo.delete_productcategory")
+def delete_category(request, slug):
+
+ category = ProductCategory.objects.get(slug=slug)
+
+ if request.method == "POST":
+ category.delete()
+ messages.success(request, _("Category deleted"))
+
+ return redirect(list_products)
+
+ data = {'category': category}
+ data['action'] = request.path
+ return render(request, 'products/delete_category.html', data)
+
+
+@permission_required("servo.change_order")
+def choose_product(request, order_id, product_id=None, target_url="orders-add_product"):
+ """
+ order_id can be either Service Order or Purchase Order
+ """
+ data = {'order': order_id}
+ data['action'] = request.path
+ data['target_url'] = target_url
+
+ if request.method == "POST":
+ query = request.POST.get('q')
+
+ if len(query) > 2:
+ products = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query)
+ )
+ data['products'] = products
+
+ return render(request, 'products/choose-list.html', data)
+
+ return render(request, 'products/choose.html', data)
+
+
+def get_info(request, location, code):
+ try:
+ product = Product.objects.get(code=code)
+ inventory = Inventory.objects.filter(product=product)
+ except Product.DoesNotExist:
+ product = cache.get(code)
+
+ return render(request, 'products/get_info.html', locals())
+
+
+def update_price(request, pk):
+ product = Product.objects.get(pk=pk)
+ try:
+ GsxAccount.default(request.user)
+ product.update_price()
+ messages.success(request, _('Price info updated from GSX'))
+ except Exception, e:
+ messages.error(request, _('Failed to update price from GSX'))
+
+ return redirect(product)
diff --git a/servo/views/purchases.py b/servo/views/purchases.py
new file mode 100644
index 0000000..66e7075
--- /dev/null
+++ b/servo/views/purchases.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+
+from django.forms.models import modelform_factory
+from django.forms.models import inlineformset_factory
+
+from django.utils.translation import ugettext as _
+
+from django.shortcuts import render, redirect
+from servo.models.order import ServiceOrderItem
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from django.contrib import messages
+
+from servo.models import Product, GsxAccount, PurchaseOrder, PurchaseOrderItem
+from servo.forms import PurchaseOrderItemEditForm, PurchaseOrderSearchForm
+
+
+@permission_required("servo.change_purchaseorder")
+def list_pos(request):
+ from datetime import timedelta
+ from django.utils import timezone
+ from django.db.models import Sum
+
+ now = timezone.now()
+ data = {'title': _("Purchase Orders")}
+
+ initial = {'start_date': now - timedelta(days=30), 'end_date': now}
+ all_orders = PurchaseOrder.objects.filter(
+ created_at__range=(initial['start_date'], initial['end_date'])
+ )
+
+ form = PurchaseOrderSearchForm(initial=initial)
+
+ if request.method == 'POST':
+ all_orders = PurchaseOrder.objects.all()
+ form = PurchaseOrderSearchForm(request.POST, initial=initial)
+
+ if form.is_valid():
+ fdata = form.cleaned_data
+ reference = fdata.get('reference')
+ if reference:
+ all_orders = all_orders.filter(reference__contains=reference)
+ if fdata.get('state') == 'open':
+ all_orders = all_orders.filter(submitted_at=None)
+ if fdata.get('state') == 'submitted':
+ all_orders = all_orders.exclude(submitted_at=None)
+ if fdata.get('state') == 'received':
+ all_orders = all_orders.exclude(has_arrived=True)
+ s, e = (fdata.get('start_date'), fdata.get('end_date'))
+ if s and e:
+ all_orders = all_orders.filter(created_at__range=(s, e))
+ created_by = fdata.get('created_by')
+ if created_by:
+ all_orders = all_orders.filter(created_by=created_by)
+
+ page = request.GET.get("page")
+ paginator = Paginator(all_orders, 50)
+
+ try:
+ orders = paginator.page(page)
+ except PageNotAnInteger:
+ orders = paginator.page(1)
+ except EmptyPage:
+ orders = paginator.page(paginator.num_pages)
+
+ data['orders'] = orders
+ data['form'] = form
+ data['total'] = all_orders.aggregate(Sum('total'))
+ return render(request, "purchases/list_pos.html", data)
+
+
+@permission_required("servo.change_purchaseorder")
+def delete_from_po(request, pk, item_id):
+ # @TODO - decrement amount_ordered?
+ po = PurchaseOrder.objects.get(pk=pk)
+ poi = PurchaseOrderItem.objects.get(pk=item_id)
+ poi.delete()
+ messages.success(request, _(u'Product %s removed' % poi.product.code))
+ return redirect(po)
+
+
+@permission_required("servo.change_purchaseorder")
+def add_to_po(request, pk, product_id):
+ po = PurchaseOrder.objects.get(pk=pk)
+ product = Product.objects.get(pk=product_id)
+ po.add_product(product, 1, request.user)
+ messages.success(request, _(u"Product %s added" % product.code))
+ return redirect(edit_po, po.pk)
+
+
+def view_po(request, pk):
+ po = PurchaseOrder.objects.get(pk=pk)
+ title = _('Purchase Order %d' % po.pk)
+ return render(request, "purchases/view_po.html", locals())
+
+
+@permission_required("servo.change_purchaseorder")
+def edit_po(request, pk, item_id=None):
+
+ if pk is not None:
+ po = PurchaseOrder.objects.get(pk=pk)
+ else:
+ po = PurchaseOrder(created_by=request.user)
+
+ PurchaseOrderForm = modelform_factory(PurchaseOrder, exclude=[])
+ form = PurchaseOrderForm(instance=po)
+
+ ItemFormset = inlineformset_factory(
+ PurchaseOrder,
+ PurchaseOrderItem,
+ extra=0,
+ form=PurchaseOrderItemEditForm,
+ exclude=[]
+ )
+
+ formset = ItemFormset(instance=po)
+
+ if request.method == "POST":
+
+ form = PurchaseOrderForm(request.POST, instance=po)
+
+ if form.is_valid():
+
+ po = form.save()
+ formset = ItemFormset(request.POST, instance=po)
+
+ if formset.is_valid():
+
+ formset.save()
+ msg = _("Purchase Order %d saved" % po.pk)
+
+ if "confirm" in request.POST.keys():
+ po.submit(request.user)
+ msg = _("Purchase Order %d submitted") % po.pk
+
+ messages.success(request, msg)
+ return redirect(list_pos)
+
+ request.session['current_po'] = po
+ data = {'order': po, 'form': form}
+ data['formset'] = formset
+ data['title'] = _('Purchase Order #%d' % po.pk)
+
+ return render(request, "purchases/edit_po.html", data)
+
+
+@permission_required("servo.change_purchaseorder")
+def order_stock(request, po_id):
+ """
+ Submits the PO as a GSX Stocking Order
+ Using the default GSX account.
+ """
+ po = PurchaseOrder.objects.get(pk=po_id)
+
+ if request.method == "POST":
+ if po.submitted_at:
+ messages.error(request, _(u'Purchase Order %s has already been submitted') % po.pk)
+ return list_pos(request)
+
+ act = GsxAccount.default(request.user)
+
+ stock_order = gsxws.StockingOrder(
+ shipToCode=act.ship_to,
+ purchaseOrderNumber=po.id
+ )
+
+ for i in po.purchaseorderitem_set.all():
+ stock_order.add_part(i.code, i.amount)
+
+ try:
+ result = stock_order.submit()
+ po.supplier = "Apple"
+ po.confirmation = result.confirmationNumber
+ po.submit(request.user)
+ msg = _("Products ordered with confirmation %s" % po.confirmation)
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(list_pos)
+
+ data = {'action': request.path}
+ return render(request, "purchases/order_stock.html", data)
+
+
+@permission_required('servo.delete_purchaseorder')
+def delete_po(request, po_id):
+ po = PurchaseOrder.objects.get(pk=po_id)
+ try:
+ po.delete()
+ messages.success(request, _("Purchase Order %s deleted" % po_id))
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(list_pos)
+
+
+@permission_required('servo.add_purchaseorder')
+def create_po(request, product_id=None, order_id=None):
+ po = PurchaseOrder(created_by=request.user)
+ location = request.user.get_location()
+ po.location = location
+ po.save()
+
+ if order_id is not None:
+ po.sales_order_id = order_id
+ for i in ServiceOrderItem.objects.filter(order_id=order_id):
+ po.add_product(i, amount=1, user=request.user)
+
+ if product_id is not None:
+ product = Product.objects.get(pk=product_id)
+ po.add_product(product, amount=1, user=request.user)
+
+ messages.success(request, _("Purchase Order %d created" % po.pk))
+
+ return redirect(edit_po, po.pk)
diff --git a/servo/views/queue.py b/servo/views/queue.py
new file mode 100644
index 0000000..e7e0914
--- /dev/null
+++ b/servo/views/queue.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django import forms
+from servo.models import Queue
+from django.http import HttpResponse
+
+
+def statuses(request, queue_id):
+ """Lists available statuses for this queue"""
+ queue = Queue.objects.get(pk=queue_id)
+
+ class StatusForm(forms.Form):
+ status = forms.ModelChoiceField(queryset=queue.queuestatus_set.all())
+
+ form = StatusForm()
+ return HttpResponse(str(form['status']))
diff --git a/servo/views/rules.py b/servo/views/rules.py
new file mode 100644
index 0000000..ec193a8
--- /dev/null
+++ b/servo/views/rules.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.conf import settings
+from django.contrib import messages
+from django.utils.translation import ugettext as _
+from django.shortcuts import render, redirect, get_object_or_404
+
+from servo.models.rules import *
+
+
+def get_data(request):
+ pass
+
+
+def list_rules(request):
+ title = _('Rules')
+ object_list = Rule.objects.all()
+ return render(request, "rules/list_rules.html", locals())
+
+
+def edit_rule(request, pk=None):
+ title = _('Rules')
+ object_list = Rule.objects.all()
+
+ if pk:
+ rule = get_object_or_404(Rule, pk=pk)
+
+ if request.method == 'POST':
+ if pk:
+ rule = Rule.objects.get(pk=pk)
+ else:
+ rule = Rule()
+
+ rule.description = request.POST.get('description')
+ #rule.match = request.POST.get('description')
+ rule.save()
+
+ rule.condition_set.all().delete()
+ rule.action_set.all().delete()
+
+ keys = request.POST.getlist('condition-key')
+ values = request.POST.getlist('condition-value')
+
+ for k, v in enumerate(keys):
+ cond = Condition(rule=rule)
+ cond.key = v
+ cond.value = values[k]
+ cond.save()
+
+ keys = request.POST.getlist('action-key')
+ values = request.POST.getlist('action-value')
+
+ for k, v in enumerate(keys):
+ action = Action(rule=rule)
+ action.key = v
+ action.value = values[k]
+ action.save()
+
+
+ return render(request, "rules/form.html", locals())
+
+
+def view_rule(request, pk):
+ pass
+
+
+def delete_rule(request, pk):
+ action = request.path
+ title = _('Delete rule')
+ rule = get_object_or_404(Rule, pk=pk)
+
+ if request.method == 'POST':
+ rule.delete()
+ messages.error(request, _('Rule deleted'))
+ return redirect(list_rules)
+
+ return render(request, "generic/delete.html", locals())
diff --git a/servo/views/search.py b/servo/views/search.py
new file mode 100644
index 0000000..c61eca6
--- /dev/null
+++ b/servo/views/search.py
@@ -0,0 +1,254 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import re
+import gsxws
+
+from django.db.models import Q
+from django.core.cache import cache
+from django.shortcuts import render, redirect
+from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
+
+from django.http import QueryDict, HttpResponseRedirect
+
+from servo.models.note import Note
+from servo.models.wiki import Article
+from servo.models.device import Device
+from servo.models.product import Product
+from servo.models.common import GsxAccount
+from servo.models.purchases import PurchaseOrder
+from servo.models.order import Order, ServiceOrderItem
+
+
+def results_redirect(view, data):
+ q = QueryDict('', mutable=True)
+ q['q'] = data['query']
+ query_str = q.urlencode()
+ url = reverse(view, args=[data['what']])
+ return HttpResponseRedirect("%s?%s" % (url, query_str))
+
+
+def prepare_result_view(request):
+
+ query = request.GET.get('q')
+
+ data = {'title': _('Search results for "%s"' % query)}
+
+ data['gsx_type'] = gsxws.validate(query.upper())
+ data['query'] = query
+ data['tag_id'] = None
+ data['cat_id'] = None # Product category
+ data['group'] = 'all' # customer group
+
+ return data, query
+
+
+def list_gsx(request, what="warranty"):
+ data, query = prepare_result_view(request)
+ data['what'] = what
+ return render(request, "search/results/gsx.html", data)
+
+
+def search_gsx(request, what, arg, value):
+ if request.is_ajax():
+
+ if what == "parts" and value != "None":
+ results = []
+ GsxAccount.default(user=request.user)
+
+ try:
+ product = gsxws.Product(productName=value)
+ parts = product.parts()
+ for p in parts:
+ results.append(Product.from_gsx(p))
+ except gsxws.GsxError, e:
+ data = {'message': e}
+ return render(request, "search/results/gsx_error.html", data)
+
+ data = {'results': results}
+
+ return render(request, "search/results/gsx_%s.html" % what, data)
+
+ data = {arg: value}
+ return render(request, "search/gsx_results.html", data)
+
+
+def view_gsx_results(request, what="warranty"):
+ """
+ Searches for something from GSX. Defaults to warranty lookup.
+ GSX search strings are always UPPERCASE.
+ """
+ results = list()
+ data, query = prepare_result_view(request)
+ query = query.upper()
+
+ error_template = "search/results/gsx_error.html"
+
+ if data['gsx_type'] == "dispatchId":
+ what = "repairs"
+
+ if data['gsx_type'] == "partNumber":
+ what = "parts"
+
+ data['what'] = what
+ gsx_type = data['gsx_type']
+
+ try:
+ if request.session.get("current_queue"):
+ queue = request.session['current_queue']
+ GsxAccount.default(request.user, queue)
+ else:
+ GsxAccount.default(request.user)
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if gsx_type == "serialNumber" or "alternateDeviceId":
+ try:
+ device = Device.objects.get(sn=query)
+ except Device.DoesNotExist:
+ device = Device(sn=query)
+
+ if what == "warranty":
+ if cache.get(query):
+ result = cache.get(query)
+ else:
+ try:
+ result = Device.from_gsx(query)
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if re.match(r'iPhone', result.description):
+ result.activation = device.get_activation()
+
+ results.append(result)
+
+ if what == "parts":
+ # looking for parts
+ if gsx_type == "partNumber":
+ # ... with a part number
+ part = gsxws.Part(partNumber=query)
+
+ try:
+ partinfo = part.lookup()
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ product = Product.from_gsx(partinfo)
+ cache.set(query, product)
+ results.append(product)
+ else:
+ # ... with a serial number
+ try:
+ results = device.get_parts()
+ data['device'] = device
+ except Exception, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if what == "repairs":
+ # Looking for GSX repairs
+ if gsx_type == "serialNumber":
+ # ... with a serial number
+ try:
+ device = gsxws.Product(query)
+ results = device.repairs()
+ except gsxws.GsxError, e:
+ return render(request, "search/results/gsx_notfound.html")
+
+ elif gsx_type == "dispatchId":
+ # ... with a repair confirmation number
+ repair = gsxws.Repair(number=query)
+ try:
+ results = repair.lookup()
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if what == "repair_details":
+ repair = gsxws.Repair(number=query)
+ results = repair.details()
+ return render(request, "search/results/gsx_repair_details.html", results)
+
+ # Cache the results for quicker access later
+ cache.set('%s-%s' % (what, query), results)
+ data['results'] = results
+
+ return render(request, "search/results/gsx_%s.html" % what, data)
+
+
+def list_products(request):
+ data, query = prepare_result_view(request)
+ data['products'] = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query)
+ )
+
+ return render(request, "search/results/products.html", data)
+
+
+def list_notes(request):
+ data, query = prepare_result_view(request)
+ data['notes'] = Note.objects.filter(body__icontains=query)
+ return render(request, "search/results/notes.html", data)
+
+
+def spotlight(request):
+ """
+ Searches for anything and redirects to the "closest" result view.
+ GSX searches are done separately.
+ """
+ data, query = prepare_result_view(request)
+ data['what'] = "warranty"
+
+ if Order.objects.filter(customer__name__icontains=query).exists():
+ return list_orders(request)
+
+ if data['gsx_type'] == "serialNumber":
+ try:
+ device = Device.objects.get(sn=query)
+ return redirect(device)
+ except Device.DoesNotExist:
+ return results_redirect("search-gsx", data)
+
+ data['parts'] = ServiceOrderItem.objects.filter(sn__icontains=query)
+
+ if gsxws.validate(query, "dispatchId"):
+ try:
+ po = PurchaseOrder.objects.get(confirmation=query)
+ data['orders'] = [po.sales_order]
+ except PurchaseOrder.DoesNotExist:
+ pass
+
+ data['products'] = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query)
+ )
+
+ data['articles'] = Article.objects.filter(content__contains=query)
+
+ return render(request, "search/spotlight.html", data)
diff --git a/servo/views/shipments.py b/servo/views/shipments.py
new file mode 100644
index 0000000..9b31e93
--- /dev/null
+++ b/servo/views/shipments.py
@@ -0,0 +1,392 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import gsxws
+from decimal import *
+
+from django.utils import timezone
+from django.contrib import messages
+from django.http import HttpResponse
+from django.shortcuts import render, redirect
+from django.utils.translation import ugettext as _
+from django.forms.models import inlineformset_factory
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models import GsxAccount, ServicePart, Shipment, PurchaseOrderItem
+from servo.forms.product import PurchaseOrderItemForm, IncomingSearchForm
+from servo.forms.returns import *
+
+
+def prep_counts():
+ incoming = PurchaseOrderItem.objects.filter(received_at=None)
+ incoming = incoming.exclude(purchase_order__submitted_at=None).count()
+ pending_return = ''
+ returns = Shipment.objects.exclude(dispatched_at=None).count()
+ return locals()
+
+
+def prep_list_view(request):
+
+ from datetime import timedelta
+ now = timezone.now()
+
+ data = {'can_receive': True}
+ data['counts'] = prep_counts()
+ location = request.user.get_location()
+
+ ordered_date_range = [now - timedelta(days=30), timezone.now()]
+ received_date_range = [now - timedelta(days=30), timezone.now()]
+
+ initial = {
+ 'location': location,
+ 'ordered_start_date': ordered_date_range[0],
+ 'ordered_end_date': ordered_date_range[1],
+ }
+
+ if request.method == 'POST':
+ data['form'] = IncomingSearchForm(request.POST, initial=initial)
+ else:
+ data['form'] = IncomingSearchForm(initial=initial)
+
+ inventory = PurchaseOrderItem.objects.filter(received_at=None)
+ inventory = inventory.exclude(purchase_order__submitted_at=None)
+
+ if request.method == 'POST':
+ fdata = request.POST
+
+ loc = fdata.get('location')
+ if loc:
+ inventory = PurchaseOrderItem.objects.filter(purchase_order__location=loc)
+
+ ordered_sd = fdata.get('ordered_start_date')
+ if ordered_sd:
+ ordered_date_range[0] = ordered_sd
+ inventory = inventory.filter(purchase_order__submitted_at__range=ordered_date_range)
+
+ received_sd = fdata.get('received_start_date')
+ if received_sd:
+ received_date_range[0] = received_sd
+ inventory = inventory.filter(received_at__range=received_date_range)
+
+ conf = fdata.get('confirmation')
+ if conf:
+ inventory = PurchaseOrderItem.objects.filter(purchase_order__confirmation=conf)
+
+ service_order = fdata.get('service_order')
+ if service_order:
+ inventory = PurchaseOrderItem.objects.filter(purchase_order__sales_order__code=service_order)
+
+ page = request.GET.get("page")
+ data['count'] = inventory.count()
+ inventory = inventory.order_by('-id')
+
+ paginator = Paginator(inventory, 200)
+ data['title'] = _(u"%d incoming products") % data['count']
+
+ try:
+ inventory = paginator.page(page)
+ except PageNotAnInteger:
+ inventory = paginator.page(1)
+ except EmptyPage:
+ inventory = paginator.page(paginator.num_pages)
+
+ data['inventory'] = inventory
+
+ return data
+
+
+def list_incoming(request, shipment=None, status=""):
+ """
+ Lists purchase order items that have not arrived yet
+ """
+ data = prep_list_view(request)
+
+ if request.POST.getlist("id"):
+ count = len(request.POST.getlist("id"))
+ for i in request.POST.getlist("id"):
+ item = PurchaseOrderItem.objects.get(pk=i)
+ try:
+ item.receive(request.user)
+ except ValueError, e:
+ messages.error(request, e)
+ return redirect(list_incoming)
+
+ messages.success(request, _("%d products received") % count)
+
+ return redirect(list_incoming)
+
+ return render(request, "shipments/list_incoming.html", data)
+
+
+def view_incoming(request, pk):
+ """
+ Shows an incoming part
+ """
+ next = False
+ item = PurchaseOrderItem.objects.get(pk=pk)
+
+ data = prep_list_view(request)
+
+ data['next'] = ""
+ data['subtitle'] = item.code
+
+ try:
+ next = item.get_next_by_created_at(received_at=None)
+ data['next'] = next.pk
+ except PurchaseOrderItem.DoesNotExist:
+ pass # That was the last of them...
+
+ if request.method == "POST":
+
+ item.received_by = request.user
+ item.received_at = timezone.now()
+
+ form = PurchaseOrderItemForm(request.POST, instance=item)
+
+ if form.is_valid():
+ try:
+ item = form.save()
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+ return redirect(view_incoming, date, pk)
+
+ messages.success(request, _(u"Product %s received") % item.code)
+
+ if next:
+ return redirect(view_incoming, next.pk)
+ else:
+ return redirect(list_incoming)
+ else:
+ form = PurchaseOrderItemForm(instance=item)
+
+ data['form'] = form
+ data['item'] = item
+ data['url'] = request.path
+
+ return render(request, "products/receive_item.html", data)
+
+
+def list_returns(request, shipment=None, date=None):
+ return render(request, "shipments/list_returns.html", locals())
+
+
+def return_label(request, code, return_order):
+
+ GsxAccount.default(request.user)
+
+ try:
+ label = gsxws.Returns(return_order)
+ return HttpResponse(label.returnLabelFileData, content_type="application/pdf")
+ except Exception, e:
+ messages.add_message(request, messages.ERROR, e)
+ return redirect('products-list')
+
+
+def list_bulk_returns(request):
+ from django.db.models import Count
+ title = _("Browse Bulk Returns")
+ returns = Shipment.objects.exclude(dispatched_at=None).annotate(num_parts=Count('servicepart'))
+
+ page = request.GET.get("page")
+ paginator = Paginator(returns, 50)
+
+ try:
+ returns = paginator.page(page)
+ except PageNotAnInteger:
+ returns = paginator.page(1)
+ except EmptyPage:
+ returns = paginator.page(paginator.num_pages)
+
+ counts = prep_counts()
+ return render(request, "shipments/list_bulk_returns.html", locals())
+
+
+def view_packing_list(request, pk):
+ shipment = Shipment.objects.get(pk=pk)
+ pdf = shipment.packing_list.read()
+ return HttpResponse(pdf, content_type="application/pdf")
+
+
+def view_bulk_return(request, pk):
+ title = _("View bulk return")
+ shipment = Shipment.objects.get(pk=pk)
+ return render(request, "shipments/view_bulk_return.html", locals())
+
+
+def edit_bulk_return(request, pk=None, ship_to=None):
+ """
+ Edits the bulk return shipment before it's submitted
+ """
+ location = request.user.get_location()
+ accounts = location.get_shipto_choices()
+
+ if len(accounts) < 1:
+ messages.error(request, _(u'Location %s has no Ship-To') % location.title)
+ return redirect('products-list_products')
+
+ if not ship_to:
+ ship_to = accounts[0][0]
+ return redirect(edit_bulk_return, ship_to=ship_to)
+
+ shipment = Shipment.get_current(request.user, location, ship_to)
+
+ part_count = shipment.servicepart_set.all().count()
+ PartFormSet = inlineformset_factory(Shipment,
+ ServicePart,
+ form=BulkReturnPartForm,
+ extra=0,
+ exclude=[])
+ form = BulkReturnForm(instance=shipment)
+ formset = PartFormSet(instance=shipment)
+
+ if request.method == "POST":
+ form = BulkReturnForm(request.POST, instance=shipment)
+ if form.is_valid():
+ formset = PartFormSet(request.POST, instance=shipment)
+ if formset.is_valid():
+ shipment = form.save()
+ msg = _("Bulk return saved")
+ formset.save()
+ if "confirm" in request.POST.keys():
+ try:
+ shipment.register_bulk_return(request.user)
+ msg = _(u"Bulk return %s submitted") % shipment.return_id
+ messages.success(request, msg)
+ return redirect(view_bulk_return, shipment.pk)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(edit_bulk_return, ship_to=ship_to)
+ messages.success(request, msg)
+ return redirect(edit_bulk_return, ship_to=ship_to)
+ else:
+ messages.error(request, formset.errors)
+ else:
+ messages.error(request, form.errors)
+
+ counts = prep_counts()
+ counts['pending_return'] = len(formset)
+ title = _(u"%d parts pending return") % part_count
+ return render(request, "shipments/edit_bulk_return.html", locals())
+
+
+def remove_from_return(request, pk, part_pk):
+ """
+ Removes a part from a bulk return
+ """
+ shipment = Shipment.objects.get(pk=pk)
+ part = ServicePart.objects.get(pk=part_pk)
+
+ try:
+ shipment.toggle_part(part)
+ messages.success(request, _(u"Part %s removed from bulk return") % part.part_number)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(edit_bulk_return)
+
+
+def add_to_return(request, pk, part=None):
+ """
+ Adds a part to a bulk return
+ """
+ data = {'action': request.path}
+
+ if pk and part:
+ shipment = Shipment.objects.get(pk=pk)
+ part = ServicePart.objects.get(pk=part)
+ shipment.servicepart_set.add(part)
+ messages.success(request, _(u"Part %s added to return") % part.part_number)
+
+ return redirect(edit_bulk_return)
+
+ if request.method == "POST":
+ query = request.POST.get('q')
+ results = ServicePart.objects.filter(return_order=query)
+ data = {'shipment': pk, 'results': results}
+
+ return render(request, "shipments/add_to_return-results.html", data)
+
+ return render(request, "shipments/add_to_return.html", data)
+
+
+def update_part(request, part, return_type):
+ """
+ Update part status to GSX
+ """
+ return_type = int(return_type)
+ part = ServicePart.objects.get(pk=part)
+
+ msg = ""
+ form = ""
+ title = ""
+
+ if return_type == Shipment.RETURN_DOA:
+ title = _("Return DOA Part")
+ form = DoaPartReturnForm(part=part)
+
+ if return_type == Shipment.RETURN_GPR:
+ title = _("Return Good Part")
+ form = GoodPartReturnForm()
+
+ if return_type == Shipment.RETURN_CTS:
+ title = _("Convert to Stock")
+ msg = _("This part will be converted to regular inventory")
+ form = ConvertToStockForm(initial={'partNumber': part.part_number})
+
+ if request.method == "POST":
+
+ if return_type == Shipment.RETURN_DOA:
+ form = DoaPartReturnForm(part=part, data=request.POST)
+ if return_type == Shipment.RETURN_GPR:
+ form = GoodPartReturnForm(request.POST)
+ if return_type == Shipment.RETURN_CTS:
+ form = ConvertToStockForm(request.POST)
+
+ if form.is_valid():
+ try:
+ part.update_part(form.cleaned_data, return_type, request.user)
+ messages.success(request, _("Part updated"))
+ except Exception, e:
+ messages.error(request, e)
+ else:
+ messages.error(request, form.errors)
+
+ return redirect(part.order_item.order)
+
+ action = request.path
+ return render(request, "shipments/update_part.html", locals())
+
+
+def parts_pending_return(request, ship_to):
+ """
+ Returns the part pending return for this GSX Account
+ """
+ pass
+
+
+def verify(request, pk):
+ shipment = Shipment.objects.get(pk=pk)
+ return redirect(shipment)
diff --git a/servo/views/stats.py b/servo/views/stats.py
new file mode 100644
index 0000000..5ca66f8
--- /dev/null
+++ b/servo/views/stats.py
@@ -0,0 +1,443 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import json
+import datetime
+from datetime import timedelta
+from django.utils import timezone
+
+from django import forms
+from django.db import connection
+from django.shortcuts import render
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+from django.views.decorators.cache import cache_page
+
+from servo.stats.forms import *
+from servo.stats.queries import *
+
+from servo.models import User, Order
+
+
+class ServoTimeDelta:
+ def __init__(self, td):
+ self.td = td
+
+ def days(self):
+ return self.td.days
+
+ def workdays(self):
+ pass
+
+ def hours(self):
+ return self.td.seconds//3600
+
+ def nonzero(self):
+ return self.hours() > 0
+
+
+def prep_view(request):
+ """
+ Prepares the stats view
+ """
+ title = _('Statistics')
+ profile = request.user
+ location = request.user.location
+
+ initial = {
+ 'location' : location.pk,
+ 'end_date' : str(default_end_date),
+ 'start_date': str(default_start_date),
+ 'timescale' : default_timescale,
+ }
+
+ group = request.user.get_group()
+
+ if group:
+ initial['group'] = group.pk
+
+ # Remember the previous stats filter
+ if request.session.get('stats_filter'):
+ initial.update(request.session['stats_filter'])
+
+ request.session['stats_filter'] = initial
+
+ return locals()
+
+
+def index(request):
+ """
+ /stats/
+ """
+ data = prep_view(request)
+ form = TechieStatsForm(initial=data['initial'])
+
+ if request.method == 'POST':
+ form = TechieStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/index.html", data)
+
+
+#@cache_page(15*60)
+def data(request, query):
+ result = []
+ stats = StatsManager()
+ cursor = connection.cursor()
+ report, what = query.split('/')
+
+ locations = request.user.locations
+ params = request.session['stats_filter']
+ timescale = params.get('timescale', default_timescale)
+ location = params.get('location', request.user.location)
+
+ if params.get('location'):
+ location = Location.objects.get(pk=params['location'])
+ else:
+ location = request.user.location
+
+ try:
+ location_id = location.pk
+ except AttributeError:
+ location_id = 0
+
+ start_date = params.get('start_date', default_start_date)
+ end_date = params.get('end_date', default_end_date)
+ queues = request.user.queues.all()
+
+ try:
+ users = params.get('group').user_set
+ except AttributeError:
+ users = User.objects.filter(location=location)
+
+ if report == "sales":
+ if what == "invoices":
+ for i in queues:
+ data = stats.sales_invoices(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if what == "purchases":
+ for i in queues:
+ data = stats.sales_purchases(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if what == "parts":
+ i = 0
+ data = []
+ labels = []
+ results = stats.sales_parts_per_labtier(start_date, end_date)
+ for r in results:
+ data.append([i, r[1]])
+ labels.append([i, r[0]])
+ i += 1
+
+ result.append({'label': labels, 'data': data})
+
+ if what == "personal":
+ location_id = request.user.get_location().id
+ users = User.objects.filter(pk=request.user.pk)
+
+ for i in users.filter(is_active=True):
+ data = stats.order_runrate(timescale, location_id, i.pk, start_date, end_date)
+ result.append({'label': i.get_full_name(), 'data': data})
+
+ if what == "runrate":
+ for i in users.filter(is_active=True):
+ data = stats.order_runrate(timescale, location_id, i.pk, start_date, end_date)
+ result.append({'label': i.get_full_name(), 'data': data})
+
+ if report == "created":
+ if what == "user":
+ for i in location.user_set.all():
+ data = stats.orders_created_by(timescale,
+ location_id,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.get_full_name(), 'data': data})
+
+ if what == "location":
+ for i in locations.all():
+ data = stats.orders_created_at(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "closed":
+ if what == "location":
+ for i in locations.all():
+ data = stats.orders_closed_at(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+ if what == "queue":
+ for i in queues:
+ data = stats.orders_closed_in(
+ timescale,
+ location.pk,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if what == "count":
+ for i in queues:
+ data = stats.order_count(timescale, location_id, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "status":
+ try:
+ status = params.get('status').title
+ except AttributeError:
+ return HttpResponse(json.dumps(result))
+
+ if what == "location":
+ for i in locations.all():
+ data = stats.statuses_per_location(
+ timescale,
+ i.pk,
+ status,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+ if what == "tech":
+ for i in User.objects.filter(location=location, is_active=True):
+ data = stats.statuses_per_user(
+ timescale,
+ i.pk,
+ status,
+ start_date,
+ end_date)
+ result.append({'label': i.get_name(), 'data': data})
+
+ if report == "turnaround":
+ if what == "location":
+ for i in locations.all():
+ data = stats.turnaround_per_location(
+ timescale,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "runrate":
+ if what == "location":
+ for i in locations.all():
+ data = stats.runrate_per_location(
+ timescale,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "distribution":
+ if what == "location":
+ result = stats.distribution_per_location(start_date, end_date)
+
+ if what == "turnaround":
+ for i in queues:
+ data = stats.order_turnaround(
+ timescale,
+ location_id,
+ i.pk,
+ start_date,
+ end_date
+ )
+ result.append({'label': i.title, 'data': data})
+
+ if what == "queues":
+ cursor.execute("""SELECT q.title, COUNT(*)
+ FROM servo_order o LEFT OUTER JOIN servo_queue q on (o.queue_id = q.id)
+ WHERE (o.created_at, o.created_at) OVERLAPS (%s, %s)
+ GROUP BY q.title""", [start_date, end_date])
+
+ for k, v in cursor.fetchall():
+ k = k or _('No Queue')
+ result.append({'label': k, 'data': v})
+
+ if what == "techs":
+ for i in users.filter(is_active=True):
+ cursor.execute("""SELECT COUNT(*) as p
+ FROM servo_order o
+ WHERE user_id = %s
+ AND location_id = %s
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY user_id""", [i.pk, location_id, start_date, end_date])
+
+ for v in cursor.fetchall():
+ result.append({'label': i.username, 'data': v})
+
+ return HttpResponse(json.dumps(result))
+
+
+def sales(request):
+ data = prep_view(request)
+ form = InvoiceStatsForm(initial=data['initial'])
+
+ if request.method == 'POST':
+ form = InvoiceStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/sales.html", data)
+
+
+def queues(request):
+ data = prep_view(request)
+ form = OrderStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = OrderStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/queues.html", data)
+
+
+def locations(request):
+ data = prep_view(request)
+ form = BasicStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = BasicStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+ data['form'] = form
+ return render(request, "stats/locations.html", data)
+
+
+def statuses(request):
+ data = prep_view(request)
+ form = StatusStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = StatusStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/statuses.html", data)
+
+
+def repairs(request):
+ title = _('Repair statistics')
+ form = NewStatsForm(initial={
+ 'location': [request.user.location],
+ 'queue': request.user.queues.all()
+ })
+
+ if request.GET.get('location'):
+ results = []
+ form = NewStatsForm(request.GET)
+ totals = {
+ 'created' : 0,
+ 'assigned' : 0,
+ 'repairs' : 0,
+ 'dispatched' : 0,
+ 'tmp_orders' : [],
+ 'turnaround' : timedelta(),
+ }
+
+ if not form.is_valid():
+ return render(request, "stats/newstats.html", locals())
+
+ cdata = form.cleaned_data
+ date_range = (cdata['start_date'], cdata['end_date'])
+
+ for u in User.active.filter(location=cdata['location']):
+ r = {'name': u.get_full_name()}
+
+ # Look at invoices first because that data may be different from
+ # assignment info (tech A startx, tech B finishes)
+ dispatched = u.invoice_set.filter(
+ order__queue=cdata['queue'],
+ order__location=cdata['location'],
+ created_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ dispatched = dispatched.filter(order__tags=cdata['label'])
+
+ # Count each case's dispatch only once
+ r['dispatched'] = dispatched.values('order_id').distinct().count()
+
+ created = u.created_orders.filter(
+ queue=cdata['queue'],
+ location=cdata['location'],
+ created_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ created = created.filter(tags=cdata['label'])
+
+ r['created'] = created.count()
+ totals['created'] += r['created'] # add amount to totals
+
+ assigned = u.order_set.filter(
+ queue=cdata['queue'],
+ location=cdata['location'],
+ started_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ assigned = assigned.filter(tags=cdata['label'])
+
+ r['assigned'] = assigned.count()
+
+ if (r['assigned'] < 1) and (r['dispatched'] < 1):
+ continue # ... only continue with actual techs
+
+ repairs = u.created_repairs.filter(
+ order__queue=cdata['queue'],
+ order__location=cdata['location'],
+ submitted_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ repairs = repairs.filter(order__tags=cdata['label'])
+
+ # Only count each case's GSX repair once
+ r['repairs'] = repairs.values('order_id').distinct().count()
+
+ totals['repairs'] += r['repairs']
+ totals['assigned'] += r['assigned']
+ totals['dispatched'] += r['dispatched']
+
+ results.append(r)
+ turnaround = timedelta()
+
+ # calculate turnaround time of dispatched cases
+ for o in dispatched:
+ totals['tmp_orders'].append(o.order)
+ for s in o.order.orderstatus_set.filter(status=cdata['status']):
+ if s.finished_at is None:
+ s.finished_at = s.order.closed_at or timezone.now()
+
+ totals['turnaround'] += (s.finished_at - s.started_at)
+
+ totals['diff'] = totals['dispatched'] - totals['assigned']
+
+ if totals['dispatched'] > 0:
+ totals['turnaround'] = ServoTimeDelta(totals['turnaround']/totals['dispatched'])
+
+ return render(request, "stats/newstats.html", locals())
diff --git a/servo/views/tags.py b/servo/views/tags.py
new file mode 100644
index 0000000..3dea6d4
--- /dev/null
+++ b/servo/views/tags.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.http import HttpResponse
+from servo.models import TaggedItem
+
+
+def clear(request, pk):
+ TaggedItem.objects.get(pk=pk).delete()
+ return HttpResponse("")
+
+
+def add(request, content_type, pk, tag):
+ pass