├── public ├── .nojekyll ├── CNAME ├── robots.txt ├── favicon.ico ├── assets │ └── binders.jpg ├── humans.txt ├── crossdomain.xml └── testem.js ├── app ├── templates │ ├── client.hbs │ ├── components │ │ ├── export-button.hbs │ │ ├── import-button.hbs │ │ └── drop-down.hbs │ ├── loading.hbs │ ├── invoices │ │ ├── new.hbs │ │ └── index.hbs │ ├── accounts │ │ ├── new.hbs │ │ └── index.hbs │ ├── clients │ │ ├── new.hbs │ │ └── index.hbs │ ├── invoice │ │ ├── edit.hbs │ │ └── show.hbs │ ├── account │ │ └── edit.hbs │ ├── client │ │ └── edit.hbs │ ├── _account_form.hbs │ ├── _client_form.hbs │ ├── application.hbs │ ├── settings.hbs │ ├── home.hbs │ └── _invoice_form.hbs ├── adapters │ ├── unit.js │ ├── currency.js │ ├── language.js │ ├── tax-rate.js │ ├── numeration-type.js │ ├── fixture.js │ └── application.js ├── resolver.js ├── routes │ ├── index.js │ ├── accounts.js │ ├── clients.js │ ├── invoices.js │ ├── account.js │ ├── client.js │ ├── home.js │ ├── invoice.js │ ├── application.js │ ├── invoice │ │ ├── show.js │ │ └── edit.js │ ├── client │ │ └── edit.js │ ├── account │ │ └── edit.js │ ├── clients │ │ └── new.js │ ├── accounts │ │ └── new.js │ ├── settings.js │ └── invoices │ │ └── new.js ├── controllers │ ├── accounts.js │ ├── clients.js │ ├── invoices.js │ ├── accounts │ │ ├── new.js │ │ └── index.js │ ├── client │ │ └── edit.js │ ├── clients │ │ ├── new.js │ │ └── index.js │ ├── account │ │ └── edit.js │ ├── invoices │ │ ├── index.js │ │ └── new.js │ ├── application.js │ ├── invoice │ │ └── edit.js │ └── settings.js ├── serializers │ └── firebase.js ├── services │ ├── dummy-session.js │ ├── firebase.js │ └── current-session.js ├── helpers │ └── format-cents.js ├── models │ ├── language.js │ ├── numeration-type.js │ ├── account.js │ ├── user.js │ ├── item.js │ ├── settings.js │ ├── tax-rate.js │ ├── client.js │ ├── invoice.js │ ├── unit.js │ ├── currency.js │ └── exchange-rates-table.js ├── initializers │ ├── session.js │ └── route.js ├── instance-initializers │ └── inject-owner-to-form.js ├── lib │ ├── parse_cents.js │ ├── format_cents.js │ └── firebase_auth.js ├── app.js ├── components │ ├── import-button.js │ ├── export-button.js │ ├── drop-down.js │ └── cents-field.js ├── forms │ ├── account.js │ ├── client.js │ ├── item.js │ ├── settings.js │ └── invoice.js ├── mixins │ ├── new-controller.js │ ├── edit-controller.js │ ├── item-properties.js │ ├── form.js │ ├── exchange-rate.js │ └── invoice-properties.js ├── router.js ├── index.html └── styles │ └── app.less ├── .prettierrc ├── .watchmanconfig ├── tests ├── .eslintrc.js ├── test-helper.js ├── helpers │ ├── destroy-app.js │ ├── resolver.js │ ├── start-app.js │ ├── auth.js │ └── module-for-acceptance.js ├── acceptance │ ├── invoices-test.js │ └── clients-test.js ├── pages │ ├── new-client.js │ └── clients.js ├── unit │ ├── invoice-test.js │ └── services │ │ └── current-session-test.js └── index.html ├── lib ├── .eslintrc.js ├── ember-md5 │ ├── package.json │ └── index.js ├── ember-bootstrap │ ├── package.json │ └── index.js └── ember-polish-to-words │ ├── package.json │ └── index.js ├── .firebaserc ├── jsconfig.json ├── mirage ├── serializers │ ├── client.js │ └── application.js ├── scenarios │ └── default.js ├── factories │ ├── user.js │ └── client.js └── config.js ├── config ├── targets.js └── environment.js ├── vendor ├── shims │ ├── md5.js │ └── polish-to-words.js └── toword.js ├── .gitignore ├── firebase.json ├── .ember-cli ├── .eslintrc.js ├── .travis.yml ├── testem.js ├── .editorconfig ├── ember-cli-build.js ├── LICENSE ├── README.md └── package.json /public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | fakturama.pl 2 | -------------------------------------------------------------------------------- /app/templates/client.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /app/adapters/unit.js: -------------------------------------------------------------------------------- 1 | export { default } from './fixture'; 2 | -------------------------------------------------------------------------------- /app/templates/components/export-button.hbs: -------------------------------------------------------------------------------- 1 | Exportuj dane 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /app/adapters/currency.js: -------------------------------------------------------------------------------- 1 | export { default } from './fixture'; 2 | -------------------------------------------------------------------------------- /app/adapters/language.js: -------------------------------------------------------------------------------- 1 | export { default } from './fixture'; 2 | -------------------------------------------------------------------------------- /app/adapters/tax-rate.js: -------------------------------------------------------------------------------- 1 | export { default } from './fixture'; 2 | -------------------------------------------------------------------------------- /app/adapters/numeration-type.js: -------------------------------------------------------------------------------- 1 | export { default } from './fixture'; 2 | -------------------------------------------------------------------------------- /app/templates/loading.hbs: -------------------------------------------------------------------------------- 1 |

Ładuję…

2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qoobaa/fakturama/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/assets/binders.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qoobaa/fakturama/HEAD/public/assets/binders.jpg -------------------------------------------------------------------------------- /lib/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: false 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /app/templates/components/import-button.hbs: -------------------------------------------------------------------------------- 1 | Importuj dane 2 | {{input type="file" change=(action 'fileChange')}} 3 | -------------------------------------------------------------------------------- /lib/ember-md5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-md5", 3 | "keywords": [ 4 | "ember-addon" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/ember-bootstrap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-bootstrap", 3 | "keywords": [ 4 | "ember-addon" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "staging": "fakturama-staging", 4 | "production": "fakturama-production-61b9a" 5 | } 6 | } -------------------------------------------------------------------------------- /lib/ember-polish-to-words/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-polish-to-words", 3 | "keywords": [ 4 | "ember-addon" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | {"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | beforeModel() { 5 | this.transitionTo('invoices'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/controllers/accounts.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default Controller.extend({ 4 | sortProperties: ['name'], 5 | sortAscending: true 6 | }); 7 | -------------------------------------------------------------------------------- /app/controllers/clients.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default Controller.extend({ 4 | sortProperties: ['companyName'], 5 | sortAscending: true 6 | }); 7 | -------------------------------------------------------------------------------- /app/routes/accounts.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model() { 5 | return this.get('store').findAll('account'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/routes/clients.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model() { 5 | return this.get('store').findAll('client'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /mirage/serializers/client.js: -------------------------------------------------------------------------------- 1 | import ApplicationSerializer from './application'; 2 | 3 | export default ApplicationSerializer.extend( 4 | {}, 5 | { 6 | type: 'client' 7 | } 8 | ); 9 | -------------------------------------------------------------------------------- /app/controllers/invoices.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default Controller.extend({ 4 | sortProperties: ['issueDate', 'number'], 5 | sortAscending: true 6 | }); 7 | -------------------------------------------------------------------------------- /app/routes/invoices.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model: function() { 5 | return this.get('store').findAll('invoice'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/templates/components/drop-down.hbs: -------------------------------------------------------------------------------- 1 | {{#each options as |option|}} 2 | 5 | {{/each}} 6 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { setResolver } from 'ember-qunit'; 3 | import { start } from 'ember-cli-qunit'; 4 | 5 | setResolver(resolver); 6 | start(); 7 | -------------------------------------------------------------------------------- /app/routes/account.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model: function(params) { 5 | return this.store.findRecord('account', params.account_id); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/routes/client.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model: function(params) { 5 | return this.store.findRecord('client', params.client_id); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/routes/home.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | showLayout: false, 5 | renderTemplate() { 6 | this.render('home', { outlet: 'no-layout' }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /app/routes/invoice.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model(params) { 5 | return this.get('store').findRecord('invoice', params.invoice_id); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /config/targets.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | browsers: [ 4 | 'ie 9', 5 | 'last 1 Chrome versions', 6 | 'last 1 Firefox versions', 7 | 'last 1 Safari versions' 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /app/controllers/accounts/new.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import NewController from 'fakturama/mixins/new-controller'; 3 | 4 | export default Controller.extend(NewController, { 5 | transitionTo: 'accounts' 6 | }); 7 | -------------------------------------------------------------------------------- /app/controllers/client/edit.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import EditController from 'fakturama/mixins/edit-controller'; 3 | 4 | export default Controller.extend(EditController, { 5 | transitionTo: 'clients' 6 | }); 7 | -------------------------------------------------------------------------------- /app/controllers/clients/new.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import NewController from 'fakturama/mixins/new-controller'; 3 | 4 | export default Controller.extend(NewController, { 5 | transitionTo: 'clients' 6 | }); 7 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | 3 | export default function destroyApp(application) { 4 | run(application, 'destroy'); 5 | if (window.server) { 6 | window.server.shutdown(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/controllers/account/edit.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import EditController from 'fakturama/mixins/edit-controller'; 3 | 4 | export default Controller.extend(EditController, { 5 | transitionTo: 'accounts' 6 | }); 7 | -------------------------------------------------------------------------------- /app/serializers/firebase.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.RESTSerializer.extend({ 4 | serializeIntoHash(data, type, record, options) { 5 | Object.assign(data, this.serialize(record, options)); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /mirage/scenarios/default.js: -------------------------------------------------------------------------------- 1 | export default function(/* server */) { 2 | /* 3 | Seed your development database using your factories. 4 | This data will not be loaded in your tests. 5 | */ 6 | // server.createList('post', 10); 7 | } 8 | -------------------------------------------------------------------------------- /vendor/shims/md5.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function vendorModule() { 3 | 'use strict'; 4 | 5 | return { 6 | default: self['md5'], 7 | __esModule: true 8 | }; 9 | } 10 | 11 | define('md5', [], vendorModule); 12 | })(); 13 | -------------------------------------------------------------------------------- /app/services/dummy-session.js: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | import { resolve } from 'rsvp'; 3 | 4 | // Dummy session used by acceptance tests 5 | export default Service.extend({ 6 | setup() { 7 | return resolve(); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | -- -- 7 | 8 | # THANKS 9 | 10 | 11 | 12 | # TECHNOLOGY COLOPHON 13 | 14 | HTML5, CSS3 15 | Ember.js 16 | -------------------------------------------------------------------------------- /vendor/shims/polish-to-words.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function vendorModule() { 3 | 'use strict'; 4 | 5 | return { 6 | default: self['polishToWords'], 7 | __esModule: true 8 | }; 9 | } 10 | 11 | define('polish-to-words', [], vendorModule); 12 | })(); 13 | -------------------------------------------------------------------------------- /app/controllers/accounts/index.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | accounts: computed('content.[]', function() { 6 | return this.get('content').filterBy('isNew', false); 7 | }) 8 | }); 9 | -------------------------------------------------------------------------------- /app/controllers/clients/index.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | clients: computed('content.[]', function() { 6 | return this.get('content').filterBy('isNew', false); 7 | }) 8 | }); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # misc 11 | /.sass-cache 12 | /connect.lock 13 | /libpeerconnection.log 14 | .DS_Store 15 | Thumbs.db 16 | /coverage/* 17 | /.grunt 18 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/helpers/format-cents.js: -------------------------------------------------------------------------------- 1 | import { helper } from '@ember/component/helper'; 2 | import formatCents from 'fakturama/lib/format_cents'; 3 | 4 | export default helper(function(value, { options: { hash = {} } = {} }) { 5 | const precision = (hash || {}).precision || 2; 6 | 7 | return formatCents(value, precision); 8 | }); 9 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": true 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | server: true 4 | }, 5 | root: true, 6 | parserOptions: { 7 | ecmaVersion: 2017, 8 | sourceType: 'module' 9 | }, 10 | extends: ['prettier'], 11 | env: { 12 | browser: true 13 | }, 14 | plugins: ['prettier'], 15 | rules: { 16 | 'prettier/prettier': 'error' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /mirage/factories/user.js: -------------------------------------------------------------------------------- 1 | import { Factory, faker } from 'ember-cli-mirage'; 2 | 3 | export default Factory.extend({ 4 | isAnonymous: false, 5 | uid(i) { 6 | return `uid-${i}`; 7 | }, 8 | displayName() { 9 | return `${faker.name.firstName()} ${faker.name.lastName()}`; 10 | }, 11 | email() { 12 | return `${this.displayName.underscore()}@example.com`; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "6" 5 | 6 | sudo: false 7 | dist: trusty 8 | 9 | addons: 10 | chrome: stable 11 | before_script: 12 | - "sudo chown root /opt/google/chrome/chrome-sandbox" 13 | - "sudo chmod 4755 /opt/google/chrome/chrome-sandbox" 14 | 15 | cache: 16 | directories: 17 | - $HOME/.npm 18 | 19 | before_install: 20 | - npm config set spin false 21 | -------------------------------------------------------------------------------- /app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default Route.extend({ 5 | showLayout: true, 6 | session: service(), 7 | 8 | beforeModel: function() { 9 | return this.get('session').setup(); 10 | }, 11 | 12 | actions: { 13 | refresh() { 14 | this.refresh(); 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /tests/acceptance/invoices-test.js: -------------------------------------------------------------------------------- 1 | // import { test } from "qunit"; 2 | import moduleForAcceptance from 'fakturama/tests/helpers/module-for-acceptance'; 3 | 4 | moduleForAcceptance('Acceptance | Invoices'); 5 | 6 | // test("invoices renders", function() { 7 | // visit('/invoices'); 8 | // andThen(function() { 9 | // assert.equal(find('a.navbar-brand').text(), "Fakturama"); 10 | // }); 11 | // }); 12 | -------------------------------------------------------------------------------- /app/models/language.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { Model, attr } = DS; 4 | 5 | let Language = Model.extend({ 6 | code: attr(), 7 | name: attr() 8 | }); 9 | 10 | Language.reopenClass({ 11 | primaryKey: 'code', 12 | FIXTURES: [ 13 | { id: 1, code: 'pl', name: 'polska' }, 14 | { id: 2, code: 'plen', name: 'polsko-angielska' } 15 | ] 16 | }); 17 | 18 | export default Language; 19 | -------------------------------------------------------------------------------- /app/templates/invoices/new.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{partial "invoice_form"}} 3 | 4 |
5 |
6 | 7 | {{link-to "Anuluj" "invoices" class="btn btn-default" activeClass=null}} 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | test_page: 'tests/index.html?hidepassed', 4 | disable_watching: true, 5 | launch_in_ci: [ 6 | 'Chrome' 7 | ], 8 | launch_in_dev: [ 9 | 'Chrome' 10 | ], 11 | browser_args: { 12 | Chrome: [ 13 | '--disable-gpu', 14 | '--headless', 15 | '--remote-debugging-port=9222', 16 | '--window-size=1440,900' 17 | ] 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /app/models/numeration-type.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { Model, attr } = DS; 4 | 5 | let NumerationType = Model.extend({ 6 | code: attr(), 7 | name: attr() 8 | }); 9 | 10 | NumerationType.reopenClass({ 11 | primaryKey: 'code', 12 | FIXTURES: [ 13 | { id: 1, code: 'month', name: 'miesięczny' }, 14 | { id: 2, code: 'year', name: 'roczny' } 15 | ] 16 | }); 17 | 18 | export default NumerationType; 19 | -------------------------------------------------------------------------------- /mirage/factories/client.js: -------------------------------------------------------------------------------- 1 | import { Factory, faker } from 'ember-cli-mirage'; 2 | 3 | export default Factory.extend({ 4 | companyName() { 5 | return faker.company.companyName(); 6 | }, 7 | address() { 8 | return `${faker.address.streetAddress()}, ${faker.address.city()}, ${faker.address.country()}`; 9 | }, 10 | contactName() { 11 | return `${faker.name.firstName()} ${faker.name.lastName()}`; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /app/templates/accounts/new.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{partial "account_form"}} 3 | 4 |
5 |
6 | 7 | {{link-to "Anuluj" "accounts" class="btn btn-default" activeClass=null}} 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /app/routes/invoice/show.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model() { 5 | return this.modelFor('invoice'); 6 | }, 7 | activate() { 8 | const invoice = this.modelFor('invoice'); 9 | document.title = `${invoice.get('number')} ${invoice.get( 10 | 'buyerFirstLine' 11 | )} - Fakturama`; 12 | }, 13 | deactivate() { 14 | document.title = 'Fakturama'; 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /app/initializers/session.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import CurrentSession from 'fakturama/services/current-session'; 3 | import DummySession from 'fakturama/services/dummy-session'; 4 | 5 | export function initialize(application) { 6 | const sessionService = Ember.testing ? DummySession : CurrentSession; 7 | application.register('service:session', sessionService); 8 | } 9 | 10 | export default { 11 | name: 'session', 12 | initialize 13 | }; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /app/initializers/route.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export function initialize() { 4 | Route.reopen({ 5 | // Show layout by default, can be opt-out in the route 6 | showLayout: true, 7 | setupController() { 8 | this._super(...arguments); 9 | this.controllerFor('application').set( 10 | 'showLayout', 11 | this.get('showLayout') 12 | ); 13 | } 14 | }); 15 | } 16 | 17 | export default { 18 | initialize 19 | }; 20 | -------------------------------------------------------------------------------- /app/instance-initializers/inject-owner-to-form.js: -------------------------------------------------------------------------------- 1 | import { setOwner } from '@ember/application'; 2 | import Form from 'fakturama/mixins/form'; 3 | 4 | // Dirty hack to get owner symbol 5 | const OWNER = (function() { 6 | let finder = {}; 7 | setOwner(finder, 'owner'); 8 | return Object.keys(finder)[0]; 9 | })(); 10 | 11 | export function initialize(appInstance) { 12 | Form.reopen({ 13 | [OWNER]: appInstance 14 | }); 15 | } 16 | 17 | export default { 18 | initialize 19 | }; 20 | -------------------------------------------------------------------------------- /app/lib/parse_cents.js: -------------------------------------------------------------------------------- 1 | function parseCents(value, precision) { 2 | var integerPart, 3 | fractionalPart, 4 | parts = String(value).split('.'); 5 | 6 | if (!isFinite(precision)) { 7 | precision = 2; 8 | } 9 | 10 | integerPart = parts[0] || '0'; 11 | fractionalPart = parts[1] || '0'; 12 | fractionalPart += new Array(precision + 1).join('0'); 13 | 14 | return parseInt(integerPart + fractionalPart.substr(0, precision), 10); 15 | } 16 | 17 | export default parseCents; 18 | -------------------------------------------------------------------------------- /app/templates/clients/new.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{partial "client_form"}} 3 | 4 |
5 |
6 | 10 | {{link-to "Anuluj" "clients" class="btn btn-default" activeClass=null}} 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /app/services/firebase.js: -------------------------------------------------------------------------------- 1 | import Service, { inject as service } from '@ember/service'; 2 | import { readOnly } from '@ember/object/computed'; 3 | 4 | import config from 'fakturama/config/environment'; 5 | 6 | const { 7 | APP: { 8 | FIREBASE: { databaseURL } 9 | } 10 | } = config; 11 | 12 | export default Service.extend({ 13 | session: service(), 14 | 15 | url: databaseURL, 16 | userId: readOnly('session.currentUser.uid'), 17 | token: readOnly('session.currentUser.authToken') 18 | }); 19 | -------------------------------------------------------------------------------- /app/routes/client/edit.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import ClientForm from 'fakturama/forms/client'; 3 | 4 | export default Route.extend({ 5 | model: function() { 6 | return this.modelFor('client'); 7 | }, 8 | 9 | setupController(controller, model) { 10 | controller.set('model', ClientForm.create({ model: model })); 11 | }, 12 | 13 | deactivate() { 14 | let controller = this.controllerFor(this.routeName); 15 | controller.set('model', null); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /app/routes/account/edit.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import AccountForm from 'fakturama/forms/account'; 3 | 4 | export default Route.extend({ 5 | model: function() { 6 | return this.modelFor('account'); 7 | }, 8 | 9 | setupController(controller, model) { 10 | controller.set('model', AccountForm.create({ model: model })); 11 | }, 12 | 13 | deactivate() { 14 | let controller = this.controllerFor(this.routeName); 15 | controller.set('model', null); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /app/models/account.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import DS from 'ember-data'; 3 | 4 | const { Model, attr } = DS; 5 | 6 | export default Model.extend({ 7 | bankName: attr(), 8 | swift: attr(), 9 | number: attr(), 10 | description: attr(), 11 | name: computed('bankName', 'description', 'number', function() { 12 | if (this.get('description')) { 13 | return this.get('description'); 14 | } else { 15 | return [this.get('bankName'), this.get('number')].compact().join(' '); 16 | } 17 | }) 18 | }); 19 | -------------------------------------------------------------------------------- /app/templates/invoice/edit.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{partial "invoice_form"}} 3 | 4 |
5 |
6 | 7 | 8 | {{link-to "Anuluj" "invoice.show" form.model class="btn btn-default" activeClass=null}} 9 |
10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | import EmberObject, { computed } from '@ember/object'; 2 | import md5 from 'md5'; 3 | 4 | export default EmberObject.extend({ 5 | emailMD5: computed('email', function() { 6 | return md5(this.getWithDefault('email', '')); 7 | }), 8 | 9 | gravatarURL: computed('emailMD5', function() { 10 | return `//www.gravatar.com/avatar/${this.get('emailMD5')}?d=mm`; 11 | }), 12 | 13 | name: computed('displayName', 'email', function() { 14 | return this.get('displayName') || this.get('email') || 'Gość'; 15 | }) 16 | }); 17 | -------------------------------------------------------------------------------- /app/models/item.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { inject as service } from '@ember/service'; 3 | import ItemPropertiesMixin from 'fakturama/mixins/item-properties'; 4 | 5 | const attributes = [ 6 | 'description', 7 | 'quantity', 8 | 'unitCode', 9 | 'netPrice', 10 | 'netAmount', 11 | 'taxRateCode', 12 | 'taxAmount', 13 | 'grossAmount' 14 | ]; 15 | 16 | export default EmberObject.extend(ItemPropertiesMixin, { 17 | store: service('store'), 18 | toJSON() { 19 | return this.getProperties(attributes); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Inflector from 'ember-inflector'; 3 | import Resolver from './resolver'; 4 | import loadInitializers from 'ember-load-initializers'; 5 | import config from './config/environment'; 6 | 7 | const inflector = Inflector.inflector; 8 | 9 | inflector.uncountable('settings'); 10 | 11 | const App = Application.extend({ 12 | modulePrefix: config.modulePrefix, 13 | podModulePrefix: config.podModulePrefix, 14 | Resolver 15 | }); 16 | 17 | loadInitializers(App, config.modulePrefix); 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /app/adapters/fixture.js: -------------------------------------------------------------------------------- 1 | import FixtureAdapter from 'ember-data-fixture-adapter'; 2 | 3 | export default FixtureAdapter.extend({ 4 | findRecord(store, model, id) { 5 | return this.find(store, model, id); 6 | }, 7 | 8 | queryRecord(store, model, query) { 9 | return this.findQuery(store, model, query); 10 | }, 11 | 12 | queryFixtures(fixtures, query) { 13 | return fixtures.find(function(record) { 14 | const compareTo = Object.assign({}, record, query); 15 | return JSON.stringify(record) === JSON.stringify(compareTo); 16 | }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /app/routes/clients/new.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import ClientForm from 'fakturama/forms/client'; 3 | 4 | export default Route.extend({ 5 | model() { 6 | return this.get('store').createRecord('client'); 7 | }, 8 | 9 | setupController(controller, model) { 10 | controller.set('model', ClientForm.create({ model: model })); 11 | }, 12 | 13 | deactivate() { 14 | let controller = this.controllerFor(this.routeName); 15 | let model = controller.get('model'); 16 | controller.set('model', null); 17 | model.delete(); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /app/templates/account/edit.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{partial "account_form"}} 3 | 4 |
5 |
6 | 7 | 8 | {{link-to "Anuluj" "accounts" class="btn btn-default" activeClass=null}} 9 |
10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /app/controllers/invoices/index.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | invoices: computed('content.@each.isNew', function() { 6 | return this.get('content').filterBy('isNew', false); 7 | }), 8 | 9 | actions: { 10 | markAsPaidInvoice: function(invoice) { 11 | invoice.set('isPaid', true); 12 | invoice.save(); 13 | }, 14 | 15 | markAsUnpaidInvoice: function(invoice) { 16 | invoice.set('isPaid', false); 17 | invoice.save(); 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /app/routes/accounts/new.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import AccountForm from 'fakturama/forms/account'; 3 | 4 | export default Route.extend({ 5 | model: function() { 6 | return this.get('store').createRecord('account'); 7 | }, 8 | 9 | setupController(controller, model) { 10 | controller.set('model', AccountForm.create({ model: model })); 11 | }, 12 | 13 | deactivate() { 14 | let controller = this.controllerFor(this.routeName); 15 | let model = controller.get('model'); 16 | controller.set('model', null); 17 | model.delete(); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { merge } from '@ember/polyfills'; 3 | import Application from '../../app'; 4 | import config from '../../config/environment'; 5 | 6 | export default function startApp(attrs) { 7 | let attributes = merge({}, config.APP); 8 | attributes = merge(attributes, attrs); // use defaults, but you can override; 9 | 10 | return run(() => { 11 | let application = Application.create(attributes); 12 | application.setupForTesting(); 13 | application.injectTestHelpers(); 14 | return application; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /app/components/import-button.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | tagName: 'a', 5 | 6 | classNames: ['btn', 'btn-default', 'btn-upload'], 7 | 8 | reset() {}, 9 | 10 | actions: { 11 | fileChange(event) { 12 | let fileReader = new window.FileReader(); 13 | if (event.target.files.length) { 14 | fileReader.onload = event => { 15 | this.get('import')(event.target.result); 16 | this.reset(); 17 | }; 18 | fileReader.readAsText(event.target.files[0]); 19 | } 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /lib/ember-bootstrap/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var Funnel = require('broccoli-funnel'); 6 | 7 | module.exports = { 8 | name: 'ember-bootstrap', 9 | 10 | included() { 11 | this._super.included(...arguments); 12 | this.import('vendor/ember-bootstrap/bootstrap.js'); 13 | }, 14 | 15 | treeForVendor() { 16 | return new Funnel( 17 | path.join(this.app.project.nodeModulesPath, 'bootstrap', 'dist', 'js'), 18 | { 19 | destDir: 'ember-bootstrap', 20 | files: ['bootstrap.js'] 21 | } 22 | ); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /app/routes/settings.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import RSVP from 'rsvp'; 3 | import SettingsForm from 'fakturama/forms/settings'; 4 | 5 | export default Route.extend({ 6 | model: function() { 7 | const store = this.get('store'); 8 | return RSVP.hash({ 9 | model: store.findRecord('settings', 'default'), 10 | numerationTypes: store.findAll('numeration-type') 11 | }); 12 | }, 13 | 14 | setupController: function(controller, models) { 15 | models.model = SettingsForm.create({ model: models.model }); 16 | models.isDeleteModalVisible = false; 17 | controller.setProperties(models); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /app/forms/account.js: -------------------------------------------------------------------------------- 1 | import ObjectProxy from '@ember/object/proxy'; 2 | import { oneWay } from '@ember/object/computed'; 3 | import EmberValidations from 'ember-validations'; 4 | import FormMixin from 'fakturama/mixins/form'; 5 | 6 | export default ObjectProxy.extend(EmberValidations, FormMixin, { 7 | validations: { 8 | number: { 9 | presence: { 10 | if: 'isSubmitted', 11 | message: 'nie może być pusty' 12 | } 13 | } 14 | }, 15 | 16 | id: oneWay('model.id'), 17 | bankName: oneWay('model.bankName'), 18 | swift: oneWay('model.swift'), 19 | number: oneWay('model.number'), 20 | description: oneWay('model.description') 21 | }); 22 | -------------------------------------------------------------------------------- /app/models/settings.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import DS from 'ember-data'; 3 | 4 | const { Model, attr } = DS; 5 | 6 | let Settings = Model.extend({ 7 | companyName: attr(), 8 | address: attr(), 9 | vatin: attr(), 10 | contactName: attr(), 11 | numerationTypeCode: attr(), 12 | dueDays: attr(), 13 | 14 | seller: computed('address', 'companyName', 'vatin', function() { 15 | var parts = [this.get('companyName'), this.get('address')]; 16 | 17 | if (this.get('vatin')) { 18 | parts.push('NIP / VATIN: ' + this.get('vatin')); 19 | } 20 | 21 | return parts.join('\n').trim(); 22 | }) 23 | }); 24 | 25 | export default Settings; 26 | -------------------------------------------------------------------------------- /lib/ember-md5/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var Funnel = require('broccoli-funnel'); 6 | var MergeTrees = require('broccoli-merge-trees'); 7 | 8 | module.exports = { 9 | name: 'ember-md5', 10 | 11 | included() { 12 | this._super.included(...arguments); 13 | this.import('vendor/ember-md5/md5.js'); 14 | this.import('vendor/shims/md5.js'); 15 | }, 16 | 17 | treeForVendor(vendorTree) { 18 | return new Funnel( 19 | path.join(this.app.project.nodeModulesPath, 'blueimp-md5', 'js'), 20 | { 21 | destDir: 'ember-md5', 22 | files: ['md5.js'] 23 | } 24 | ); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/ember-polish-to-words/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var Funnel = require('broccoli-funnel'); 6 | 7 | module.exports = { 8 | name: 'ember-polish-to-words', 9 | 10 | included() { 11 | this._super.included(...arguments); 12 | this.import('vendor/ember-polish-to-words/index.js'); 13 | this.import('vendor/shims/polish-to-words.js'); 14 | }, 15 | 16 | treeForVendor() { 17 | return new Funnel( 18 | path.join(this.app.project.nodeModulesPath, 'polish-to-words'), 19 | { 20 | destDir: 'ember-polish-to-words', 21 | files: ['index.js'] 22 | } 23 | ); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/templates/client/edit.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{partial "client_form"}} 3 | 4 |
5 |
6 | 10 | 14 | {{link-to "Anuluj" "clients" class="btn btn-default" activeClass=null}} 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /tests/helpers/auth.js: -------------------------------------------------------------------------------- 1 | import User from 'fakturama/models/user'; 2 | 3 | let application = null; 4 | 5 | export function setup(app) { 6 | application = app; 7 | } 8 | 9 | export function reset() { 10 | let session = application.__container__.lookup('service:session'); 11 | session.set('currentUser', null); 12 | } 13 | 14 | export function authenticate(user, token) { 15 | let session = application.__container__.lookup('service:session'); 16 | session.set( 17 | 'currentUser', 18 | User.create({ 19 | uid: user.uid, 20 | authToken: token, 21 | email: user.email, 22 | displayName: user.displayName, 23 | isAnonymous: user.isAnonymous 24 | }) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/mixins/new-controller.js: -------------------------------------------------------------------------------- 1 | import { typeOf } from '@ember/utils'; 2 | import Mixin from '@ember/object/mixin'; 3 | import { oneWay } from '@ember/object/computed'; 4 | 5 | export default Mixin.create({ 6 | errors: oneWay('model.errors'), 7 | 8 | makeTransition() { 9 | let transitionTo = this.get('transitionTo'); 10 | if (typeOf(transitionTo) == 'function') { 11 | transitionTo.call(this); 12 | } else { 13 | this.transitionToRoute(transitionTo); 14 | } 15 | }, 16 | 17 | actions: { 18 | saveRecord: function() { 19 | let model = this.get('model'); 20 | 21 | model.set('isSubmitted', true); 22 | model.save().then(() => this.makeTransition(), () => null); 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /app/models/tax-rate.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { Model, attr } = DS; 4 | 5 | let TaxRate = Model.extend({ 6 | code: attr(), 7 | value: attr(), 8 | name: attr(), 9 | nameEN: attr() 10 | }); 11 | 12 | TaxRate.reopenClass({ 13 | primaryKey: 'code', 14 | FIXTURES: [ 15 | { id: 1, code: '23', value: 23, name: '23%', nameEN: null }, 16 | { id: 2, code: '8', value: 8, name: '8%', nameEN: null }, 17 | { id: 3, code: '5', value: 5, name: '5%', nameEN: null }, 18 | { id: 4, code: '0', value: 0, name: '0%', nameEN: null }, 19 | { id: 5, code: 'na', value: 0, name: 'n.p.', nameEN: 'n/a' }, 20 | { id: 6, code: 'exempt', value: 0, name: 'zw.', nameEN: 'exempt' } 21 | ] 22 | }); 23 | 24 | export default TaxRate; 25 | -------------------------------------------------------------------------------- /public/testem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is dummy file that exists for the sole purpose 3 | * of allowing tests to run directly in the browser as 4 | * well as by Testem. 5 | * 6 | * Testem is configured to run tests directly against 7 | * the test build of index.html, which requires a 8 | * snippet to load the testem.js file: 9 | * 10 | * This has to go after the qunit framework and app 11 | * tests are loaded. 12 | * 13 | * Testem internally supplies this file. However, if you 14 | * run the tests directly in the browser (localhost:8000/tests), 15 | * this file does not exist. 16 | * 17 | * Hence the purpose of this fake file. This file is served 18 | * directly from the express server to satisify the script load. 19 | */ 20 | -------------------------------------------------------------------------------- /app/forms/client.js: -------------------------------------------------------------------------------- 1 | import ObjectProxy from '@ember/object/proxy'; 2 | import { oneWay } from '@ember/object/computed'; 3 | import EmberValidations from 'ember-validations'; 4 | import FormMixin from 'fakturama/mixins/form'; 5 | 6 | export default ObjectProxy.extend(EmberValidations, FormMixin, { 7 | validations: { 8 | companyName: { 9 | presence: { 10 | if: 'isSubmitted', 11 | message: 'nie może być pusta' 12 | } 13 | }, 14 | address: { 15 | presence: { 16 | if: 'isSubmitted', 17 | message: 'nie może być pusty' 18 | } 19 | } 20 | }, 21 | 22 | id: oneWay('model.id'), 23 | companyName: oneWay('model.companyName'), 24 | address: oneWay('model.address'), 25 | vatin: oneWay('model.vatin') 26 | }); 27 | -------------------------------------------------------------------------------- /app/components/export-button.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default Component.extend({ 5 | tagName: 'a', 6 | 7 | attributeBindings: ['href', 'download'], 8 | 9 | classNames: ['btn btn-default'], 10 | 11 | firebase: service('firebase'), 12 | 13 | click() { 14 | const { url, userId, token } = this.get('firebase').getProperties([ 15 | 'url', 16 | 'userId', 17 | 'token' 18 | ]); 19 | const date = new Date() 20 | .toISOString() 21 | .substr(0, 16) 22 | .replace('T', '_'); 23 | 24 | this.setProperties({ 25 | href: `${url}/${userId}.json?auth=${token}`, 26 | download: `fakturama-export-${date}.json` 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /app/lib/format_cents.js: -------------------------------------------------------------------------------- 1 | function formatCents(value, precision) { 2 | var integerPart, 3 | fractionalPart, 4 | minus = ''; 5 | 6 | if (!isFinite(precision)) { 7 | precision = 2; 8 | } 9 | 10 | if (!value) { 11 | value = '0'; 12 | } 13 | 14 | value = String(value); 15 | 16 | if (value[0] === '-') { 17 | minus = '-'; 18 | value = value.substr(1); 19 | } 20 | 21 | value = new Array(precision + 1).join('0').slice(value.length - 1) + value; 22 | integerPart = value.substr(0, value.length - precision); 23 | fractionalPart = value.slice(-precision); 24 | 25 | if (precision > 0) { 26 | return minus + [integerPart, fractionalPart].join('.'); 27 | } else { 28 | return minus + integerPart; 29 | } 30 | } 31 | 32 | export default formatCents; 33 | -------------------------------------------------------------------------------- /app/mixins/edit-controller.js: -------------------------------------------------------------------------------- 1 | import { typeOf } from '@ember/utils'; 2 | import Mixin from '@ember/object/mixin'; 3 | 4 | export default Mixin.create({ 5 | makeTransition() { 6 | let transitionTo = this.get('transitionTo'); 7 | if (typeOf(transitionTo) == 'function') { 8 | transitionTo.call(this); 9 | } else { 10 | this.transitionToRoute(transitionTo); 11 | } 12 | }, 13 | actions: { 14 | saveRecord: function() { 15 | let model = this.get('model'); 16 | 17 | model.set('isSubmitted', true); 18 | model.save().then(() => this.makeTransition(), () => null); 19 | }, 20 | 21 | deleteRecord: function() { 22 | let model = this.get('model'); 23 | 24 | model.delete(true).then(() => this.makeTransition()); 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /tests/pages/new-client.js: -------------------------------------------------------------------------------- 1 | import { create, clickable, fillable } from 'ember-cli-page-object'; 2 | 3 | export default create({ 4 | companyName: fillable('[data-test-client-company-name]'), 5 | address: fillable('[data-test-client-address]'), 6 | vatin: fillable('[data-test-client-vatin]'), 7 | contactName: fillable('[data-test-client-contact-name]'), 8 | contactEmail: fillable('[data-test-client-contact-email]'), 9 | delete: clickable('[data-test-client-delete]'), 10 | submit: clickable('[data-test-client-save]'), 11 | 12 | saveWith(attrs) { 13 | return this.companyName(attrs.companyName) 14 | .address(attrs.address) 15 | .vatin(attrs.vatin) 16 | .contactName(attrs.contactName) 17 | .contactEmail(attrs.contactEmail) 18 | .submit(); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /app/models/client.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import DS from 'ember-data'; 3 | 4 | const { Model, attr } = DS; 5 | 6 | export default Model.extend({ 7 | companyName: attr(), 8 | address: attr(), 9 | vatin: attr(), 10 | contactName: attr(), 11 | contactEmail: attr(), 12 | gravatarURL: computed('contactEmail', function() { 13 | const md5 = window.md5(this.getWithDefault('contactEmail', '')); 14 | return `//www.gravatar.com/avatar/${md5}?d=mm`; 15 | }), 16 | buyer: computed('address', 'companyName', 'vatin', function() { 17 | let parts = [this.get('companyName'), this.get('address')]; 18 | 19 | if (this.get('vatin')) { 20 | parts.push('NIP / VATIN: ' + this.get('vatin')); 21 | } 22 | 23 | return parts.join('\n').trim(); 24 | }) 25 | }); 26 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | 3 | import startApp from './start-app'; 4 | import destroyApp from './destroy-app'; 5 | import { setup as setupAuth, reset as resetAuth } from './auth'; 6 | 7 | export default function(name, options = {}) { 8 | module(name, { 9 | beforeEach() { 10 | this.application = startApp(); 11 | setupAuth(this.application); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | resetAuth(); 20 | 21 | if (options.afterEach) { 22 | return options.afterEach 23 | .apply(this, arguments) 24 | .then(() => destroyApp(this.application)); 25 | } else { 26 | destroyApp(this.application); 27 | } 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /mirage/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | import { RestSerializer } from 'ember-cli-mirage'; 3 | 4 | export default RestSerializer.extend({ 5 | embed: true, 6 | root: false, 7 | 8 | normalize(payload) { 9 | const type = this.type; 10 | assert(type, 'type must be provided in the serializer'); 11 | return RestSerializer.prototype.normalize.call(this, { 12 | [type]: { 13 | payload 14 | } 15 | }); 16 | }, 17 | 18 | serialize() { 19 | let json = RestSerializer.prototype.serialize.apply(this, arguments); 20 | if (json.id) { 21 | json.name = json.id; 22 | } 23 | if (Array.isArray(json)) { 24 | return json.reduce(function(memo, e) { 25 | memo[e.id] = e; 26 | return memo; 27 | }, {}); 28 | } else { 29 | return json; 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /app/models/invoice.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import InvoicePropertiesMixin from 'fakturama/mixins/invoice-properties'; 3 | 4 | const { Model, attr } = DS; 5 | 6 | export default Model.extend(InvoicePropertiesMixin, { 7 | itemsAttributes: attr(), 8 | number: attr('string'), 9 | issueDate: attr('string'), 10 | deliveryDate: attr('string'), 11 | dueDate: attr('string'), 12 | seller: attr('string'), 13 | buyer: attr('string'), 14 | currencyCode: attr('string'), 15 | languageCode: attr('string'), 16 | comment: attr('string'), 17 | sellerSignature: attr('string'), 18 | buyerSignature: attr('string'), 19 | exchangeRate: attr('number'), 20 | exchangeDate: attr('string'), 21 | exchangeDivisor: attr('number'), 22 | accountBankName: attr('string'), 23 | accountSwift: attr('string'), 24 | accountNumber: attr('string'), 25 | isPaid: attr('boolean') 26 | }); 27 | -------------------------------------------------------------------------------- /app/routes/invoice/edit.js: -------------------------------------------------------------------------------- 1 | import { hash } from 'rsvp'; 2 | import Route from '@ember/routing/route'; 3 | import InvoiceForm from 'fakturama/forms/invoice'; 4 | 5 | export default Route.extend({ 6 | model() { 7 | const store = this.get('store'); 8 | 9 | return hash({ 10 | model: this.modelFor('invoice'), 11 | currencies: store.findAll('currency'), 12 | taxRates: store.findAll('tax-rate'), 13 | languages: store.findAll('language'), 14 | units: store.findAll('unit'), 15 | clients: store.findAll('client'), 16 | accounts: store.findAll('account') 17 | }); 18 | }, 19 | 20 | setupController(controller, models) { 21 | models.model = InvoiceForm.create({ model: models.model }); 22 | controller.setProperties(models); 23 | }, 24 | 25 | deactivate() { 26 | let controller = this.controllerFor(this.routeName); 27 | controller.set('model', null); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /app/templates/clients/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if content}} 2 |
    3 | {{#each clients as |client|}} 4 |
  • 5 | {{#link-to "client.edit" client class="pull-left"}} 6 | 7 | {{/link-to}} 8 |
    9 |

    10 | {{link-to client.contactName "client.edit" client data-test-client-visit}} 11 |

    12 |

    {{client.companyName}}

    13 |
    14 |
  • 15 | {{/each}} 16 |
17 |
18 | {{else}} 19 |

Nie dodano jeszcze żadnego klienta.

20 | {{/if}} 21 | 22 | {{link-to "Dodaj nowego klienta" "clients.new" data-test-new-client class="btn btn-success" active=null}} 23 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | this.route('home', { path: '/' }); 11 | 12 | this.route('clients', function() { 13 | this.route('new'); 14 | }); 15 | this.route('client', { path: 'client/:client_id' }, function() { 16 | this.route('edit'); 17 | }); 18 | 19 | this.route('accounts', function() { 20 | this.route('new'); 21 | }); 22 | this.route('account', { path: 'account/:account_id' }, function() { 23 | this.route('edit'); 24 | }); 25 | 26 | this.route('invoices', function() { 27 | this.route('new'); 28 | }); 29 | this.route('invoice', { path: 'invoice/:invoice_id' }, function() { 30 | this.route('show', { path: '/' }); 31 | this.route('edit'); 32 | }); 33 | 34 | this.route('settings'); 35 | }); 36 | 37 | export default Router; 38 | -------------------------------------------------------------------------------- /app/templates/accounts/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if accounts}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{#each accounts as |account|}} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{/each}} 22 | 23 |
OpisNazwa bankuNumer rachunkuSWIFT
{{account.description}}{{account.bankName}}{{account.number}}{{account.swift}}{{link-to "Edytuj" "account.edit" account}}
24 | {{else}} 25 |

Nie dodano jeszcze żadnego rachunku bankowego.

26 | {{/if}} 27 | 28 | {{link-to "Dodaj nowy rachunek bankowy" "accounts.new" class="btn btn-success" active=null}} 29 | -------------------------------------------------------------------------------- /tests/unit/invoice-test.js: -------------------------------------------------------------------------------- 1 | // import Invoice from "fakturama/models/invoice"; 2 | // import TaxRate from "fakturama/models/tax_rate"; 3 | 4 | // var invoice; 5 | 6 | // module("Unit - Invoice", { 7 | // setup: function () { 8 | // TaxRate.fetch(); 9 | // invoice = Invoice.create({ 10 | // itemsAttributes: [] 11 | // }); 12 | // } 13 | // }); 14 | 15 | // test("calculates tax correctly", function () { 16 | // expect(3); 17 | 18 | // Ember.run(function () { 19 | // invoice.set("itemsAttributes", [{ 20 | // taxRateCode: "23", 21 | // netPrice: 24390, 22 | // quantity: 1 23 | // }, { 24 | // taxRateCode: "23", 25 | // netPrice: 24390, 26 | // quantity: 1 27 | // }]); 28 | 29 | // strictEqual(invoice.get("totalNetAmount"), 48780); 30 | // strictEqual(invoice.get("totalTaxAmount"), 11219); 31 | // strictEqual(invoice.get("totalGrossAmount"), 59999); 32 | // }); 33 | // }); 34 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 5 | 6 | module.exports = function(defaults) { 7 | let app = new EmberApp(defaults, { 8 | lessOptions: { 9 | paths: ['node_modules'] 10 | } 11 | }); 12 | 13 | app.import('vendor/firebase.js'); 14 | app.import('vendor/toword.js'); 15 | 16 | // Use `app.import` to add additional libraries to the generated 17 | // output files. 18 | // 19 | // If you need to use different assets in different 20 | // environments, specify an object as the first parameter. That 21 | // object's keys should be the environment name and the values 22 | // should be the asset to use in that environment. 23 | // 24 | // If the library that you are including contains AMD or ES6 25 | // modules that you would like to import into your application 26 | // please specify an object with the list of modules as keys 27 | // along with the exports of each module as its value. 28 | 29 | return app.toTree(); 30 | }; 31 | -------------------------------------------------------------------------------- /app/mixins/item-properties.js: -------------------------------------------------------------------------------- 1 | import Mixin from '@ember/object/mixin'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Mixin.create({ 5 | netAmount: computed('netPrice', 'quantity', function() { 6 | return Math.round(this.get('netPrice') * this.get('quantity')); 7 | }), 8 | 9 | taxAmount: computed('netAmount', 'taxRate.value', function() { 10 | return Math.round( 11 | (this.get('netAmount') * this.get('taxRate.value')) / 100 12 | ); 13 | }), 14 | 15 | grossAmount: computed('netAmount', 'taxAmount', function() { 16 | return this.get('netAmount') + this.get('taxAmount'); 17 | }), 18 | 19 | unit: computed('unitCode', function() { 20 | const code = this.get('unitCode'); 21 | if (code) { 22 | return this.get('store').queryRecord('unit', { code }); 23 | } 24 | }), 25 | 26 | taxRate: computed('taxRateCode', function() { 27 | const code = this.get('taxRateCode'); 28 | 29 | if (code) { 30 | return this.get('store').queryRecord('tax-rate', { code }); 31 | } 32 | }) 33 | }); 34 | -------------------------------------------------------------------------------- /app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { inject as service } from '@ember/service'; 3 | import { readOnly } from '@ember/object/computed'; 4 | 5 | export default Controller.extend({ 6 | session: service(), 7 | 8 | user: readOnly('session.currentUser'), 9 | 10 | isAlertDismissed: false, 11 | 12 | actions: { 13 | signIn: function(method) { 14 | const session = this.get('session'); 15 | session.create(method).then( 16 | () => { 17 | this.clearCache(); 18 | this.send('refresh'); 19 | }, 20 | error => alert(error.message) 21 | ); 22 | }, 23 | 24 | signOut: function() { 25 | const session = this.get('session'); 26 | session.remove().then(() => { 27 | this.clearCache(); 28 | this.transitionToRoute('home'); 29 | }); 30 | }, 31 | 32 | dismissAlert: function() { 33 | this.set('isAlertDismissed', true); 34 | } 35 | }, 36 | 37 | clearCache: function() { 38 | this.get('store').unloadAll(); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /tests/pages/clients.js: -------------------------------------------------------------------------------- 1 | import { 2 | attribute, 3 | create, 4 | clickable, 5 | collection, 6 | text, 7 | visitable 8 | } from 'ember-cli-page-object'; 9 | import { reject } from 'rsvp'; 10 | 11 | export default create({ 12 | visit: visitable('/clients'), 13 | newClient: clickable('[data-test-new-client]'), 14 | showClient(id) { 15 | const client = this.client(id); 16 | if (client) { 17 | return client.visit(); 18 | } else { 19 | reject(`Client ${id} not found`); 20 | } 21 | }, 22 | clients: collection('[data-test-clients]', { 23 | id: attribute('data-test-client-id', 'div'), 24 | contactName: text('[data-test-client-contact-name]'), 25 | companyName: text('[data-test-client-company-name]'), 26 | visit: clickable('[data-test-client-visit]') 27 | }), 28 | client(id = null) { 29 | const clients = this.clients; 30 | if (id) { 31 | return clients.filter(c => c.id === id).objectAt(0); 32 | } else { 33 | return clients.objectAt(clients.length - 1); 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Kuba Kuźma 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/lib/firebase_auth.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | 3 | import config from 'fakturama/config/environment'; 4 | 5 | const { 6 | APP: { 7 | FIREBASE, 8 | FIREBASE: { projectId } 9 | } 10 | } = config; 11 | const { firebase } = window; 12 | const app = firebase.initializeApp(FIREBASE); 13 | 14 | function delegateMethod(name) { 15 | return function() { 16 | return this.get('content')[name](...arguments); 17 | }; 18 | } 19 | 20 | export default EmberObject.extend({ 21 | init() { 22 | this._super(...arguments); 23 | this.set('content', app.auth(projectId)); 24 | this.set('googleProvider', new firebase.auth.GoogleAuthProvider()); 25 | }, 26 | 27 | onAuthStateChanged: delegateMethod('onAuthStateChanged'), 28 | signInAnonymously: delegateMethod('signInAnonymously'), 29 | signInWithGoogle() { 30 | const content = this.get('content'); 31 | const provider = this.get('googleProvider'); 32 | provider.addScope('email'); 33 | provider.addScope('profile'); 34 | return content.signInWithPopup(provider); 35 | }, 36 | signOut: delegateMethod('signOut') 37 | }); 38 | -------------------------------------------------------------------------------- /app/models/unit.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { Model, attr } = DS; 4 | 5 | let Unit = Model.extend({ 6 | code: attr(), 7 | name: attr(), 8 | nameEN: attr() 9 | }); 10 | 11 | Unit.reopenClass({ 12 | primaryKey: 'code', 13 | FIXTURES: [ 14 | { id: 1, code: 'hour', name: 'godz.', nameEN: 'hrs' }, 15 | { id: 2, code: 'service', name: 'usł.', nameEN: 'service' }, 16 | { id: 3, code: 'piece', name: 'szt.', nameEN: 'pcs' }, 17 | { id: 4, code: 'day', name: 'dni', nameEN: 'days' }, 18 | { id: 5, code: 'discount', name: 'rabat', nameEN: 'discount' }, 19 | { id: 6, code: 'kilogram', name: 'kg', nameEN: 'kg' }, 20 | { id: 7, code: 'ton', name: 'ton', nameEN: 'tons' }, 21 | { id: 8, code: 'metre', name: 'm', nameEN: 'm' }, 22 | { id: 9, code: 'kilometre', name: 'km', nameEN: 'km' }, 23 | { id: 10, code: 'advance', name: 'zaliczka', nameEN: 'advance' }, 24 | { id: 11, code: 'set', name: 'komplet', nameEN: 'set' }, 25 | { id: 12, code: 'squaremetre', name: 'm²', nameEN: 'm²' }, 26 | { id: 13, code: 'cubicmetre', name: 'm³', nameEN: 'm³' } 27 | ] 28 | }); 29 | 30 | export default Unit; 31 | -------------------------------------------------------------------------------- /app/models/currency.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import DS from 'ember-data'; 3 | 4 | const { Model, attr } = DS; 5 | 6 | let Currency = Model.extend({ 7 | code: attr(), 8 | name: attr(), 9 | precision: attr(), 10 | nameWithCode: computed('code', 'name', function() { 11 | return '%@ (%@)'.fmt(this.get('name'), this.get('code')); 12 | }) 13 | }); 14 | 15 | Currency.reopenClass({ 16 | primaryKey: 'code', 17 | FIXTURES: [ 18 | { id: 1, code: 'PLN', name: 'złoty', precision: 2 }, 19 | { id: 2, code: 'GBP', name: 'funt szterling', precision: 2 }, 20 | { id: 3, code: 'USD', name: 'dolar amerykański', precision: 2 }, 21 | { id: 4, code: 'EUR', name: 'euro', precision: 2 }, 22 | { id: 5, code: 'CHF', name: 'frank szwajcarski', precision: 2 }, 23 | { id: 6, code: 'CZK', name: 'korona czeska', precision: 2 }, 24 | { id: 7, code: 'NOK', name: 'korona norweska', precision: 2 }, 25 | { id: 8, code: 'SEK', name: 'korona szwedzka', precision: 2 }, 26 | { id: 9, code: 'CAD', name: 'dolar kanadyjski', precision: 2 }, 27 | { id: 10, code: 'DKK', name: 'korona duńska', precision: 2 } 28 | ] 29 | }); 30 | 31 | export default Currency; 32 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fakturama 7 | 8 | 9 | 10 | 11 | {{content-for "head"}} 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | 18 | 19 | 26 | 27 | {{content-for "body"}} 28 | 29 | 30 | 31 | 32 | {{content-for "body-footer"}} 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fakturama Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/components/drop-down.js: -------------------------------------------------------------------------------- 1 | import { next } from '@ember/runloop'; 2 | import Component from '@ember/component'; 3 | import { computed } from '@ember/object'; 4 | 5 | export default Component.extend({ 6 | tagName: 'select', 7 | 8 | attributeBindings: ['id'], 9 | 10 | classNames: ['form-control'], 11 | 12 | didReceiveAttrs() { 13 | this._super(...arguments); 14 | 15 | // Pass first option's value to onSelect if value is undefined 16 | if (this.get('value') === undefined) { 17 | const valueKey = this.get('valueKey'); 18 | const model = this.get('model.firstObject'); 19 | if (model) { 20 | const value = model.get(valueKey); 21 | next(this, function() { 22 | this.get('onSelect')(value); 23 | }); 24 | } 25 | } 26 | }, 27 | 28 | change(event) { 29 | const value = event.target.value; 30 | this.get('onSelect')(value); 31 | }, 32 | 33 | options: computed('model.@each', 'labelKey', 'valueKey', function() { 34 | const label = this.get('labelKey'); 35 | const value = this.get('valueKey'); 36 | return this.getWithDefault('model', []).map(item => { 37 | return { value: item.get(value), label: item.get(label) }; 38 | }); 39 | }) 40 | }); 41 | -------------------------------------------------------------------------------- /app/mixins/form.js: -------------------------------------------------------------------------------- 1 | import { alias } from '@ember/object/computed'; 2 | import { resolve } from 'rsvp'; 3 | import Mixin from '@ember/object/mixin'; 4 | 5 | export default Mixin.create({ 6 | model: alias('content'), 7 | isSubmitted: false, 8 | 9 | save: function() { 10 | var form = this, 11 | model = this.get('model'); 12 | 13 | return this.validate().then(function() { 14 | model.setProperties(form.toJSON()); 15 | return model.save(); 16 | }, null); 17 | }, 18 | 19 | delete(persist = false) { 20 | const model = this.get('model'); 21 | if (model.isDestroying || model.isDestroyed) { 22 | return resolve(true); 23 | } 24 | 25 | if (model.get('isNew')) { 26 | return model.unloadRecord(); 27 | } else if (persist) { 28 | return model.destroyRecord(); 29 | } 30 | }, 31 | 32 | toJSON: function() { 33 | return this.getProperties(Object.keys(this.get('model').toJSON())); 34 | }, 35 | 36 | addErrors: function(errors) { 37 | var form = this; 38 | 39 | Object.keys(errors || {}).forEach(function(property) { 40 | form.set( 41 | 'errors.' + property, 42 | form.get('errors.' + property).concat(errors[property]) 43 | ); 44 | }); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /app/routes/invoices/new.js: -------------------------------------------------------------------------------- 1 | import { hash } from 'rsvp'; 2 | // import { next } from "@ember/runloop"; 3 | import Route from '@ember/routing/route'; 4 | import InvoiceForm from 'fakturama/forms/invoice'; 5 | 6 | export default Route.extend({ 7 | model() { 8 | const store = this.get('store'); 9 | 10 | return hash({ 11 | model: store.createRecord('invoice'), 12 | currencies: store.findAll('currency'), 13 | taxRates: store.findAll('tax-rate'), 14 | languages: store.findAll('language'), 15 | units: store.findAll('unit'), 16 | settings: store.findRecord('settings', 'default'), 17 | clients: store.findAll('client'), 18 | accounts: store.findAll('account'), 19 | invoices: this.modelFor('invoices') 20 | }); 21 | }, 22 | 23 | setupController(controller, models) { 24 | const currencyCode = models.currencies.get('firstObject.code'); 25 | models.model.set('currencyCode', currencyCode); 26 | models.model = InvoiceForm.create({ model: models.model }); 27 | controller.setProperties(models); 28 | }, 29 | 30 | deactivate() { 31 | let controller = this.controllerFor(this.routeName); 32 | let model = controller.get('model'); 33 | controller.set('model', null); 34 | model.delete(); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /app/forms/item.js: -------------------------------------------------------------------------------- 1 | import ObjectProxy from '@ember/object/proxy'; 2 | import { oneWay } from '@ember/object/computed'; 3 | import EmberValidations from 'ember-validations'; 4 | import ItemPropertiesMixin from 'fakturama/mixins/item-properties'; 5 | import FormMixin from 'fakturama/mixins/form'; 6 | 7 | export default ObjectProxy.extend( 8 | EmberValidations, 9 | FormMixin, 10 | ItemPropertiesMixin, 11 | { 12 | validations: { 13 | description: { 14 | presence: { if: 'invoiceForm.isSubmitted' } 15 | }, 16 | quantity: { 17 | presence: { if: 'invoiceForm.isSubmitted' }, 18 | numericality: { if: 'invoiceForm.isSubmitted' } 19 | }, 20 | netPrice: { 21 | presence: { if: 'invoiceForm.isSubmitted' }, 22 | numericality: { if: 'invoiceForm.isSubmitted' } 23 | }, 24 | unitCode: { 25 | presence: { if: 'invoiceForm.isSubmitted' } 26 | }, 27 | taxRateCode: { 28 | presence: { if: 'invoiceForm.isSubmitted' } 29 | } 30 | }, 31 | 32 | description: oneWay('model.description'), 33 | quantity: oneWay('model.quantity'), 34 | netPrice: oneWay('model.netPrice'), 35 | unitCode: oneWay('model.unitCode'), 36 | taxRateCode: oneWay('model.taxRateCode') 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /app/components/cents-field.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed } from '@ember/object'; 3 | import formatCents from 'fakturama/lib/format_cents'; 4 | import parseCents from 'fakturama/lib/parse_cents'; 5 | 6 | const defaultPrecision = 2; 7 | 8 | export default Component.extend({ 9 | tagName: 'input', 10 | 11 | attributeBindings: ['type', 'step', 'value', 'disabled'], 12 | 13 | type: 'number', 14 | 15 | precision: defaultPrecision, 16 | 17 | init() { 18 | this._super(...arguments); 19 | this.assignValue(); 20 | }, 21 | 22 | didReceiveAttrs() { 23 | this._super(...arguments); 24 | this.assignValue(); 25 | }, 26 | 27 | change(event) { 28 | const { 29 | target: { value } 30 | } = event; 31 | const precision = this.get('precision'); 32 | const cents = parseCents(value, precision); 33 | this.attrs.cents.update(cents); 34 | }, 35 | 36 | step: computed('precision', function() { 37 | const precision = parseInt(this.get('precision') || defaultPrecision, 10); 38 | 39 | return String(1 / Math.pow(10, precision)); 40 | }), 41 | 42 | assignValue() { 43 | const cents = this.get('cents'); 44 | const precision = this.get('precision'); 45 | this.set('value', formatCents(cents, precision)); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /app/models/exchange-rates-table.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import $ from 'jquery'; 3 | 4 | let ExchangeRatesTable = EmberObject.extend({ 5 | isLoading: false, 6 | isError: false 7 | }); 8 | 9 | ExchangeRatesTable.reopenClass({ 10 | QUERY: 11 | "SELECT * FROM nbp.tables WHERE id IN (SELECT id FROM nbp.dir WHERE typ = 'A' AND data_publikacji < '%@' | SORT(field='data_publikacji') | TAIL(count=1))", 12 | 13 | find: function(issueDate) { 14 | var model = this.create({ isLoading: true }); 15 | 16 | $.ajax('https://query.yahooapis.com/v1/public/yql', { 17 | data: { 18 | format: 'json', 19 | q: this.QUERY.fmt(issueDate), 20 | env: 'store://datatables.org/alltableswithkeys' 21 | } 22 | }) 23 | .then(function(response) { 24 | if (response.query.results) { 25 | return response.query.results.tabela_kursow; 26 | } else { 27 | return $.Deferred().reject(response); 28 | } 29 | }) 30 | .done(function(response) { 31 | model.setProperties(response); 32 | }) 33 | .fail(function() { 34 | model.set('isError', true); 35 | }) 36 | .always(function() { 37 | model.set('isLoading', false); 38 | }); 39 | 40 | return model; 41 | } 42 | }); 43 | 44 | export default ExchangeRatesTable; 45 | -------------------------------------------------------------------------------- /app/controllers/invoice/edit.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import EditController from 'fakturama/mixins/edit-controller'; 3 | 4 | export default Controller.extend(EditController, { 5 | transitionTo() { 6 | const model = this.get('content.model'); 7 | if (model.get('isDeleted')) { 8 | this.transitionToRoute('invoices'); 9 | } else { 10 | this.transitionToRoute('invoice.show', model); 11 | } 12 | }, 13 | settings: null, 14 | currencies: null, 15 | taxRates: null, 16 | languages: null, 17 | units: null, 18 | clients: null, 19 | accounts: null, 20 | 21 | isRemoveItemDisabled: function() { 22 | return this.get('items.length') <= 1; 23 | }.property('items.@each'), 24 | 25 | actions: { 26 | addItem() { 27 | this.get('content').addItem(); 28 | }, 29 | 30 | removeItem(item) { 31 | this.get('items').removeObject(item); 32 | }, 33 | 34 | chooseClient(client) { 35 | this.get('model').setProperties({ 36 | buyer: client.get('buyer'), 37 | buyerSignature: client.get('contactName') 38 | }); 39 | }, 40 | 41 | chooseAccount(account) { 42 | this.get('model').setProperties({ 43 | accountBankName: account.get('bankName'), 44 | accountSwift: account.get('swift'), 45 | accountNumber: account.get('number') 46 | }); 47 | } 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /app/forms/settings.js: -------------------------------------------------------------------------------- 1 | import ObjectProxy from '@ember/object/proxy'; 2 | import { oneWay } from '@ember/object/computed'; 3 | import EmberValidations from 'ember-validations'; 4 | import FormMixin from 'fakturama/mixins/form'; 5 | 6 | export default ObjectProxy.extend(EmberValidations, FormMixin, { 7 | validations: { 8 | companyName: { 9 | presence: { 10 | if: 'isSubmitted', 11 | message: 'nie może być pusta' 12 | } 13 | }, 14 | address: { 15 | presence: { 16 | if: 'isSubmitted', 17 | message: 'nie może być pusty' 18 | } 19 | }, 20 | vatin: { 21 | presence: { 22 | if: 'isSubmitted', 23 | message: 'nie może być pusty' 24 | } 25 | }, 26 | dueDays: { 27 | presence: { 28 | if: 'isSubmitted', 29 | message: 'nie może być pusty' 30 | }, 31 | numericality: { 32 | if: 'isSubmitted', 33 | greaterThanOrEqualTo: 0, 34 | messages: { 35 | greaterThanOrEqualTo: 'nie może być ujemny' 36 | } 37 | } 38 | } 39 | }, 40 | 41 | id: oneWay('model.id'), 42 | companyName: oneWay('model.companyName'), 43 | address: oneWay('model.address'), 44 | vatin: oneWay('model.vatin'), 45 | dueDays: oneWay('model.dueDays'), 46 | 47 | initDueDays: function() { 48 | if (this.get('dueDays') === undefined) { 49 | this.set('dueDays', 14); 50 | } 51 | }.on('init') 52 | }); 53 | -------------------------------------------------------------------------------- /mirage/config.js: -------------------------------------------------------------------------------- 1 | const idWithoutJSONSuffix = request => { 2 | return request.params.id.split('.')[0]; 3 | }; 4 | 5 | export default function() { 6 | // These comments are here to help you get started. Feel free to delete them. 7 | 8 | /* 9 | Config (with defaults). 10 | 11 | Note: these only affect routes defined *after* them! 12 | */ 13 | 14 | // this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server 15 | // this.namespace = ''; // make this `/api`, for example, if your API is namespaced 16 | // this.timing = 400; // delay for each request, automatically set to 0 during testing 17 | 18 | /* 19 | Shorthand cheatsheet: 20 | 21 | this.get('/posts'); 22 | this.post('/posts'); 23 | this.get('/posts/:id'); 24 | this.put('/posts/:id'); // or this.patch 25 | this.del('/posts/:id'); 26 | 27 | http://www.ember-cli-mirage.com/docs/v0.3.x/shorthands/ 28 | */ 29 | this.get('/clients.json', 'clients'); 30 | this.post('/clients.json', 'clients'); 31 | this.put('/clients/:id', function({ clients }, request) { 32 | const id = idWithoutJSONSuffix(request); 33 | const attrs = this.normalizedRequestAttrs(); 34 | return clients.find(id).update(attrs.payload); 35 | }); 36 | this.del('/clients/:id', function({ clients }, request) { 37 | const id = idWithoutJSONSuffix(request); 38 | return clients.find(id).destroy(); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /app/mixins/exchange-rate.js: -------------------------------------------------------------------------------- 1 | import Mixin from '@ember/object/mixin'; 2 | import { observer } from '@ember/object'; 3 | import ExchangeRatesTable from 'fakturama/models/exchange-rates-table'; 4 | 5 | export default Mixin.create({ 6 | prevIssueDate: null, 7 | 8 | issueDateDidChange: observer('issueDate', function() { 9 | const issueDate = this.get('issueDate'); 10 | const prevIssueDate = this.get('prevIssueDate'); 11 | 12 | // Hack to avoid re-assigning issueDate and exchange attributes if not needed 13 | if (issueDate && prevIssueDate !== issueDate) { 14 | this.set('prevIssueDate', issueDate); 15 | this.set( 16 | 'exchangeRateTable', 17 | ExchangeRatesTable.find(this.get('issueDate')) 18 | ); 19 | } 20 | }), 21 | 22 | exchangeRateCurrencyDidChange: observer( 23 | 'currencyCode', 24 | 'exchangeRateTable.pozycja', 25 | function() { 26 | const currency = this.getWithDefault( 27 | 'exchangeRateTable.pozycja', 28 | [] 29 | ).findBy('kod_waluty', this.get('currencyCode')); 30 | let exchangeDate = null; 31 | let exchangeRate = null; 32 | let exchangeDivisor = null; 33 | 34 | if (currency) { 35 | exchangeDate = this.get('exchangeRateTable.data_publikacji'); 36 | exchangeRate = parseInt(currency.kurs_sredni.replace(',', ''), 10); 37 | exchangeDivisor = parseInt(currency.przelicznik, 10); 38 | } 39 | this.setProperties({ 40 | exchangeDate: exchangeDate, 41 | exchangeRate: exchangeRate, 42 | exchangeDivisor: exchangeDivisor 43 | }); 44 | } 45 | ) 46 | }); 47 | -------------------------------------------------------------------------------- /app/controllers/settings.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Controller, { inject as controller } from '@ember/controller'; 3 | import { inject as service } from '@ember/service'; 4 | import { readOnly } from '@ember/object/computed'; 5 | 6 | export default Controller.extend({ 7 | application: controller(), 8 | 9 | firebase: service('firebase'), 10 | 11 | numerationTypes: null, 12 | 13 | isDeleteModalVisible: false, 14 | 15 | errors: readOnly('model.errors'), 16 | 17 | actions: { 18 | save: function() { 19 | let model = this.get('model'); 20 | model.set('isSubmitted', true); 21 | model.save().then(() => this.transitionToRoute('invoices.index')); 22 | }, 23 | 24 | showDeleteModal: function() { 25 | this.set('isDeleteModalVisible', true); 26 | }, 27 | 28 | dismissDeleteModal: function() { 29 | this.set('isDeleteModalVisible', false); 30 | }, 31 | 32 | importDatabase(data) { 33 | const { url, userId, token } = this.get('firebase').getProperties([ 34 | 'url', 35 | 'userId', 36 | 'token' 37 | ]); 38 | const uri = `${url}/${userId}.json?auth=${token}`; 39 | 40 | $.ajax(uri, { type: 'PUT', data: data }).done(() => { 41 | this.get('application').clearCache(); 42 | this.transitionToRoute('invoices'); 43 | }); 44 | }, 45 | 46 | deleteDatabase() { 47 | const { url, userId, token } = this.get('firebase').getProperties([ 48 | 'url', 49 | 'userId', 50 | 'token' 51 | ]); 52 | const uri = `${url}/${userId}.json?auth=${token}`; 53 | 54 | $.ajax(uri, { type: 'DELETE' }).done(() => { 55 | this.get('application').clearCache(); 56 | this.transitionToRoute('invoices'); 57 | }); 58 | } 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import Ember from 'ember'; 3 | import Inflector from 'ember-inflector'; 4 | import { readOnly } from '@ember/object/computed'; 5 | import { inject as service } from '@ember/service'; 6 | 7 | const { RESTAdapter } = DS; 8 | 9 | const host = Ember.testing ? '/' : readOnly('firebase.url'); 10 | const inflector = new Inflector(Inflector.defaultRules); 11 | const namespace = Ember.testing ? '' : readOnly('firebase.userId'); 12 | 13 | export default RESTAdapter.extend({ 14 | host, 15 | namespace, 16 | defaultSerializer: 'firebase', 17 | firebase: service('firebase'), 18 | 19 | buildURL() { 20 | const url = `${this._super(...arguments)}.json?auth=${this.get( 21 | 'firebase.token' 22 | )}`; 23 | return url.startsWith('http') ? url : `/${url}`; 24 | }, 25 | 26 | createRecord(store, model) { 27 | return this._super(...arguments).then(payload => { 28 | const record = Object.assign({}, { id: payload.name }); 29 | return { [inflector.pluralize(model.modelName)]: record }; 30 | }); 31 | }, 32 | 33 | findAll(store, model) { 34 | return this._super(...arguments).then(payload => { 35 | const records = Object.keys(payload || {}).map(id => { 36 | return Object.assign({}, payload[id], { id }); 37 | }); 38 | return { [inflector.pluralize(model.modelName)]: records }; 39 | }); 40 | }, 41 | 42 | findRecord(store, model, id) { 43 | return this._super(...arguments).then(payload => { 44 | const record = Object.assign({}, payload, { id }); 45 | return { [inflector.pluralize(model.modelName)]: record }; 46 | }); 47 | }, 48 | 49 | updateRecord(store, model, snapshot) { 50 | return this._super(...arguments).then(payload => { 51 | const record = Object.assign({}, payload, { id: snapshot.id }); 52 | return { [inflector.pluralize(model.modelName)]: record }; 53 | }); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /app/templates/invoices/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if content}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{#each invoices as |invoice|}} 16 | 17 | 20 | 23 | 26 | 29 | 37 | 44 | 47 | 48 | {{/each}} 49 | 50 |
NumerData wystawieniaTermin płatnościNabywcaKwota bruttoOpłacona?
18 | {{invoice.number}} 19 | 21 | {{invoice.issueDate}} 22 | 24 | {{invoice.dueDate}} 25 | 27 | {{invoice.buyerFirstLine}} 28 | 30 | {{format-cents invoice.totalGrossAmount}} 31 | {{invoice.currencyCode}} 32 | {{#if invoice.isForeignCurrency}} 33 |
{{format-cents invoice.totalGrossAmountPLN}} 34 | PLN 35 | {{/if}} 36 |
38 | {{#if invoice.isPaid}} 39 | tak 40 | {{else}} 41 | nie 42 | {{/if}} 43 | 45 | {{link-to "Zobacz podgląd" "invoice.show" invoice}} 46 |
51 | {{else}} 52 |

Nie utworzono jeszcze żadnej faktury.

53 | {{/if}} 54 | 55 | {{link-to "Utwórz nową fakturę" "invoices.new" class="btn btn-success" active=null}} 56 | -------------------------------------------------------------------------------- /app/templates/_account_form.hbs: -------------------------------------------------------------------------------- 1 |
2 | Rachunek bankowy 3 | 4 |
5 | 8 |
9 | {{input id="account-bank-name" class="form-control" 10 | value=(mut (get model 'bankName'))}} 11 |
12 |
13 | 14 | {{errors.bankName.firstObject}} 15 | 16 |
17 |
18 | 19 |
20 | 23 |
24 | {{input id="account-swift" class="form-control" 25 | value=(mut (get model 'swift'))}} 26 |
27 |
28 | 29 | {{errors.swift.firstObject}} 30 | 31 |
32 |
33 | 34 |
35 | 38 |
39 | {{input id="account-number" class="form-control" 40 | value=(mut (get model 'number'))}} 41 |
42 |
43 | 44 | {{errors.number.firstObject}} 45 | 46 |
47 |
48 | 49 |
50 | 53 |
54 | {{input id="account-description" class="form-control" 55 | value=(mut (get model 'description'))}} 56 |
57 |
58 | 59 | {{errors.description.firstObject}} 60 | 61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fakturama ![build](https://travis-ci.org/lowski/fakturama.svg?branch=master) 2 | 3 | Fakturama to prosta aplikacja do fakturowania, napisana w całości w języku JavaScript, z wykorzystaniem frameworka [Ember](http://emberjs.com/). Dane składowane są w bazie danych [Firebase](https://www.firebase.com/), kursy walut ściąganę są za pośrednictwem [YQL](http://developer.yahoo.com/yql/) z serwerów [Narodowego Banku Polskiego](http://www.nbp.pl/kursy/xml/). Ogólnodostępna wersja aplikacji znajduje się pod adresem [https://fakturama.pl/](https://fakturama.pl/), ale nic nie stoi na przeszkodzie, aby uruchomić własną. 4 | 5 | ## Uruchamianie środowiska programistycznego 6 | 7 | Aby uruchomić aplikację lokalnie, musisz posiadać node.js (wersja w okolicy 8.9.0) i yarn (okolice 1.3.2). Wszelkie zależności aplikacji instalowane są po wywołaniu `yarn install`. Aby uruchomić lokalną wersję serwera w środowisku `development`, należy uruchomić polecenie `ember server` i otworzyć w przeglądarce adres [http://localhost:8000/](http://localhost:8000/). 8 | 9 | ## Budowanie wersji produkcyjnej 10 | 11 | Do budowania wersji produkcyjnej aplikacji służy polecenie `ember build --environment production`. Po wykonaniu polecenia w katalogu `dist` powinno znajdować się kilka plików, wśród których najważniejsze to: 12 | 13 | * `index.html` - strona startowa 14 | * `assets/fakturama.xxxxxxxx.css` - arkusz styli 15 | * `assets/fakturama.xxxxxxxx.js` - kod źródłowy aplikacji 16 | * `assets/vendor.xxxxxxxx.js` - kod źródłowy zależności (zewnętrznych bibliotek) 17 | * `assets/vendor.xxxxxxxx.css` - arkusz styli zależności 18 | 19 | ## Publikowanie 20 | 21 | Ogólnodostępna wersja hostowana jest przy pomocy [Firebase Hosting](https://firebase.google.com/) i [Cloudflare](https://www.cloudflare.com/). Do publikowania aplikacji służy polecenie `ember build --environment production && firebase use production && firebase deploy`. 22 | 23 | ## Licencja 24 | 25 | Autorem Fakturamy jest [Kuba Kuźma](https://kubakuzma.com/). Kod aplikacji udostępniany jest na zasadach licencji [MIT](https://raw.githubusercontent.com/cowbell/fakturama/master/LICENSE). 26 | -------------------------------------------------------------------------------- /tests/acceptance/clients-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from 'fakturama/tests/helpers/module-for-acceptance'; 3 | import clientsPage from 'fakturama/tests/pages/clients'; 4 | import newClientPage from 'fakturama/tests/pages/new-client'; 5 | import { authenticate } from 'fakturama/tests/helpers/auth'; 6 | 7 | moduleForAcceptance('Acceptance | clients', { 8 | beforeEach() { 9 | const user = server.create('user'); 10 | authenticate(user, 'token'); 11 | } 12 | }); 13 | 14 | test('creating a new client', function(assert) { 15 | clientsPage.visit().newClient(); 16 | 17 | andThen(function() { 18 | const attrs = { 19 | companyName: 'Simpsons LLC', 20 | address: 'Springfield', 21 | contactName: 'Bart Simpson' 22 | }; 23 | newClientPage.saveWith(attrs); 24 | 25 | andThen(function() { 26 | const client = clientsPage.client(); 27 | assert.equal(client.contactName, attrs.contactName); 28 | assert.equal(client.companyName, attrs.companyName); 29 | }); 30 | }); 31 | }); 32 | 33 | test('editing a client', function(assert) { 34 | const client = server.create('client'); 35 | clientsPage.visit(); 36 | 37 | andThen(function() { 38 | clientsPage.showClient(client.id); 39 | 40 | andThen(function() { 41 | const attrs = Object.assign({}, client, { 42 | companyName: 'Simpsons LLC', 43 | address: 'Springfield', 44 | contactName: 'Bart Simpson' 45 | }); 46 | newClientPage.saveWith(attrs); 47 | 48 | andThen(function() { 49 | const client = clientsPage.client(); 50 | assert.equal(client.contactName, attrs.contactName); 51 | assert.equal(client.companyName, attrs.companyName); 52 | }); 53 | }); 54 | }); 55 | }); 56 | 57 | test('removing a client', function(assert) { 58 | const client = server.create('client'); 59 | clientsPage.visit(); 60 | 61 | andThen(function() { 62 | clientsPage.showClient(client.id); 63 | 64 | andThen(function() { 65 | newClientPage.delete(); 66 | 67 | andThen(function() { 68 | const clients = clientsPage.clients; 69 | assert.equal(clients.length, 0); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/unit/services/current-session-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { resolve } from 'rsvp'; 3 | import { moduleFor, test } from 'ember-qunit'; 4 | 5 | moduleFor('service:current-session', 'Unit | Service | session'); 6 | 7 | test('it creates a service with authClass', function(assert) { 8 | assert.expect(2); 9 | const authClass = EmberObject.extend({ 10 | init() { 11 | this._super(...arguments); 12 | assert.ok(this, 'authClass#create called'); 13 | } 14 | }); 15 | let service = this.subject({ authClass }); 16 | 17 | assert.ok(service); 18 | }); 19 | 20 | test('it creates a new session with provided method', function(assert) { 21 | assert.expect(2); 22 | const authClass = EmberObject.extend({ 23 | signInAnonymously() { 24 | assert.ok(this, 'authClass#signInAnonymously called'); 25 | return resolve(true); 26 | }, 27 | signInWithGoogle() { 28 | assert.ok(this, 'authClass#signInWithGoogle called'); 29 | return resolve(true); 30 | } 31 | }); 32 | 33 | let service = this.subject({ authClass }); 34 | service.create(); 35 | service.create('google'); 36 | }); 37 | 38 | test('it removes a session and then create an anonymous one', function(assert) { 39 | assert.expect(2); 40 | const authClass = EmberObject.extend({ 41 | signInAnonymously() { 42 | assert.ok(this, 'authClass#signInAnonymously called'); 43 | return resolve(); 44 | }, 45 | signOut() { 46 | assert.ok('authClass#signOut called'); 47 | return resolve(); 48 | } 49 | }); 50 | 51 | let service = this.subject({ authClass }); 52 | service.remove(); 53 | }); 54 | 55 | test('it setup a session', function(assert) { 56 | assert.expect(2); 57 | const user = EmberObject.create({ 58 | getIdToken() { 59 | return resolve('token'); 60 | } 61 | }); 62 | const authClass = EmberObject.extend({ 63 | onAuthStateChanged(callback) { 64 | callback(user); 65 | } 66 | }); 67 | 68 | let service = this.subject({ authClass }); 69 | return service.setup().then(() => { 70 | const currentUser = service.get('currentUser'); 71 | assert.ok(currentUser); 72 | assert.equal(currentUser.get('authToken'), 'token'); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /app/services/current-session.js: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | import { computed } from '@ember/object'; 3 | import { resolve, Promise as EmberPromise } from 'rsvp'; 4 | 5 | import FirebaseAuth from 'fakturama/lib/firebase_auth'; 6 | import User from 'fakturama/models/user'; 7 | 8 | // This service needs to be injected via app initializer under 'session' name. 9 | // This allow us to stub ad-hoc injection in in the acceptance tests. 10 | export default Service.extend({ 11 | init() { 12 | this._super(...arguments); 13 | 14 | const authClass = this.getWithDefault('authClass', FirebaseAuth); 15 | this.set('_auth', authClass.create()); 16 | }, 17 | 18 | setup() { 19 | if (this.getWithDefault('_setup', false)) { 20 | return resolve(); 21 | } 22 | 23 | return new EmberPromise(resolve => { 24 | const auth = this.get('_auth'); 25 | auth.onAuthStateChanged(user => { 26 | if (user) { 27 | return this._setUser(user).then(resolve); 28 | } else { 29 | return this.create().then(resolve); 30 | } 31 | }); 32 | this.set('_setup', true); 33 | }); 34 | }, 35 | 36 | currentUser: computed('userData.{user,token}', function() { 37 | const { user, token } = this.get('userData'); 38 | return User.create({ 39 | uid: user.uid, 40 | authToken: token, 41 | email: user.email, 42 | displayName: user.displayName, 43 | isAnonymous: user.isAnonymous 44 | }); 45 | }), 46 | 47 | create(method = 'anonymous') { 48 | const auth = this.get('_auth'); 49 | return new EmberPromise((resolve, reject) => { 50 | switch (method) { 51 | case 'anonymous': 52 | auth.signInAnonymously().then(resolve, reject); 53 | break; 54 | case 'google': 55 | auth.signInWithGoogle().then(resolve, reject); 56 | break; 57 | default: 58 | reject(`Session#create with ${method} is not supported`); 59 | } 60 | }); 61 | }, 62 | 63 | remove() { 64 | const auth = this.get('_auth'); 65 | return auth.signOut().then(() => this.create()); 66 | }, 67 | 68 | _setUser(user) { 69 | return user.getIdToken().then(token => { 70 | this.set('userData', { user, token }); 71 | }); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fakturama", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "author": "Kuba Kuźma", 7 | "directories": { 8 | "doc": "doc", 9 | "test": "tests" 10 | }, 11 | "repository": "", 12 | "scripts": { 13 | "build": "ember build", 14 | "precommit": "lint-staged", 15 | "start": "ember server", 16 | "test": "ember test" 17 | }, 18 | "lint-staged": { 19 | "*.{js,json,css}": [ 20 | "prettier --write", 21 | "git add" 22 | ] 23 | }, 24 | "devDependencies": { 25 | "blueimp-md5": "^2.10.0", 26 | "bootstrap": "^3.3.7", 27 | "broccoli-asset-rev": "^2.4.5", 28 | "broccoli-funnel": "^2.0.1", 29 | "broccoli-merge-trees": "^2.0.0", 30 | "ember-ajax": "^3.0.0", 31 | "ember-cli": "~2.15.1", 32 | "ember-cli-app-version": "^3.0.0", 33 | "ember-cli-babel": "^6.3.0", 34 | "ember-cli-dependency-checker": "^2.0.0", 35 | "ember-cli-eslint": "^4.0.0", 36 | "ember-cli-htmlbars": "^2.0.1", 37 | "ember-cli-htmlbars-inline-precompile": "^1.0.0", 38 | "ember-cli-inject-live-reload": "^1.4.1", 39 | "ember-cli-less": "^1.5.5", 40 | "ember-cli-mirage": "^0.4.3", 41 | "ember-cli-page-object": "^1.15.0-beta.2", 42 | "ember-cli-qunit": "^4.0.0", 43 | "ember-cli-shims": "^1.1.0", 44 | "ember-cli-sri": "^2.1.0", 45 | "ember-cli-uglify": "^1.2.0", 46 | "ember-data": "~2.15.0", 47 | "ember-data-fixture-adapter": "^1.13.0", 48 | "ember-export-application-global": "^2.0.0", 49 | "ember-load-initializers": "^1.0.0", 50 | "ember-resolver": "^4.0.0", 51 | "ember-source": "~2.15.0", 52 | "ember-test-selectors": "^0.3.8", 53 | "ember-truth-helpers": "^2.0.0", 54 | "ember-validations": "github:CassioFelippe/ember-validations", 55 | "eslint-config-prettier": "^2.9.0", 56 | "eslint-plugin-prettier": "^2.6.0", 57 | "firebase-tools": "^3.16.0", 58 | "husky": "^0.14.3", 59 | "lint-staged": "^7.2.0", 60 | "loader.js": "^4.2.3", 61 | "polish-to-words": "github:qoobaa/polish-to-words", 62 | "prettier": "^1.13.5" 63 | }, 64 | "engines": { 65 | "node": "^4.5 || 6.* || >= 7.*" 66 | }, 67 | "false": {}, 68 | "ember-addon": { 69 | "paths": [ 70 | "lib/ember-bootstrap", 71 | "lib/ember-md5", 72 | "lib/ember-polish-to-words" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /vendor/toword.js: -------------------------------------------------------------------------------- 1 | // Convert numbers to words 2 | // copyright 25th July 2006, by Stephen Chapman http://javascript.about.com 3 | // permission to use this Javascript on your web page is granted 4 | // provided that all of the code (including this copyright notice) is 5 | // used exactly as shown (you can change the numbering system if you wish) 6 | 7 | // American Numbering System 8 | var th = ['', 'thousand', 'million', 'billion', 'trillion']; 9 | // uncomment this line for English Number System 10 | // var th = ['','thousand','million','milliard','billion']; 11 | 12 | var dg = [ 13 | 'zero', 14 | 'one', 15 | 'two', 16 | 'three', 17 | 'four', 18 | 'five', 19 | 'six', 20 | 'seven', 21 | 'eight', 22 | 'nine' 23 | ]; 24 | var tn = [ 25 | 'ten', 26 | 'eleven', 27 | 'twelve', 28 | 'thirteen', 29 | 'fourteen', 30 | 'fifteen', 31 | 'sixteen', 32 | 'seventeen', 33 | 'eighteen', 34 | 'nineteen' 35 | ]; 36 | var tw = [ 37 | 'twenty', 38 | 'thirty', 39 | 'forty', 40 | 'fifty', 41 | 'sixty', 42 | 'seventy', 43 | 'eighty', 44 | 'ninety' 45 | ]; 46 | function toWords(s) { 47 | s = s.replace(/[\, ]/g, ''); 48 | if (s != parseFloat(s)) return 'not a number'; 49 | var x = s.indexOf('.'); 50 | if (x == -1) x = s.length; 51 | if (x > 15) return 'too big'; 52 | var n = s.split(''); 53 | var str = ''; 54 | var sk = 0; 55 | for (var i = 0; i < x; i++) { 56 | if ((x - i) % 3 == 2) { 57 | if (n[i] == '1') { 58 | str += tn[Number(n[i + 1])] + ' '; 59 | i++; 60 | sk = 1; 61 | } else if (n[i] != 0) { 62 | str += tw[n[i] - 2] + ' '; 63 | sk = 1; 64 | } 65 | } else if (n[i] != 0) { 66 | str += dg[n[i]] + ' '; 67 | if ((x - i) % 3 == 0) str += 'hundred '; 68 | sk = 1; 69 | } 70 | if ((x - i) % 3 == 1) { 71 | if (sk) str += th[(x - i - 1) / 3] + ' '; 72 | sk = 0; 73 | } 74 | } 75 | if (x != s.length) { 76 | var y = s.length; 77 | str += 'point '; 78 | for (var i = x + 1; i < y; i++) str += dg[n[i]] + ' '; 79 | } 80 | return str.replace(/\s+/g, ' '); 81 | } 82 | 83 | /* 84 | FILE ARCHIVED ON 02:38:55 Aug 04, 2016 AND RETRIEVED FROM THE 85 | INTERNET ARCHIVE ON 17:36:55 Jan 19, 2018. 86 | JAVASCRIPT APPENDED BY WAYBACK MACHINE, COPYRIGHT INTERNET ARCHIVE. 87 | 88 | ALL OTHER CONTENT MAY ALSO BE PROTECTED BY COPYRIGHT (17 U.S.C. 89 | SECTION 108(a)(3)). 90 | */ 91 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function(environment) { 5 | let ENV = { 6 | modulePrefix: 'fakturama', 7 | environment, 8 | rootURL: '/', 9 | locationType: 'auto', 10 | EmberENV: { 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. "with-controller": true 14 | }, 15 | EXTEND_PROTOTYPES: { 16 | // Prevent Ember Data from overriding Date.parse. 17 | Date: false 18 | } 19 | }, 20 | 21 | APP: { 22 | // Here you can pass flags/options to your application instance 23 | // when it is created 24 | } 25 | }; 26 | 27 | if (environment === 'development') { 28 | ENV['ember-cli-mirage'] = { 29 | enabled: false 30 | }; 31 | ENV.APP.FIREBASE = { 32 | apiKey: 'AIzaSyCsqfzOAhMod8CgmLQofz24JHk8lSgpZwo', 33 | authDomain: 'fakturama-development.firebaseapp.com', 34 | databaseURL: 'https://fakturama-development.firebaseio.com', 35 | projectId: 'fakturama-development', 36 | storageBucket: '', 37 | messagingSenderId: '1068753197003' 38 | }; 39 | 40 | // ENV.APP.LOG_RESOLVER = true; 41 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 42 | // ENV.APP.LOG_TRANSITIONS = true; 43 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 44 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 45 | } 46 | 47 | if (environment === 'test') { 48 | ENV.APP.FIREBASE = {}; 49 | // Testem prefers this... 50 | ENV.locationType = 'none'; 51 | 52 | // keep test console output quieter 53 | ENV.APP.LOG_ACTIVE_GENERATION = false; 54 | ENV.APP.LOG_VIEW_LOOKUPS = false; 55 | 56 | ENV.APP.rootElement = '#ember-testing'; 57 | } 58 | 59 | if (environment === 'staging') { 60 | ENV.APP.FIREBASE = { 61 | apiKey: 'AIzaSyDP6rWEbwd3F8Kd-2q97a24Nqm6DmQbuLQ', 62 | authDomain: 'fakturama-staging.firebaseapp.com', 63 | databaseURL: 'https://fakturama-staging.firebaseio.com', 64 | projectId: 'fakturama-staging', 65 | storageBucket: 'fakturama-staging.appspot.com', 66 | messagingSenderId: '230102749631' 67 | }; 68 | } 69 | 70 | if (environment === 'production') { 71 | ENV.APP.FIREBASE = { 72 | apiKey: 'AIzaSyDYHmwCeXtn6Nlj51XqUwR6KbJmw97Wo3A', 73 | authDomain: 'fakturama-production-61b9a.firebaseapp.com', 74 | databaseURL: 'https://fakturama-production-61b9a.firebaseio.com', 75 | projectId: 'fakturama-production-61b9a', 76 | storageBucket: 'fakturama-production-61b9a.appspot.com', 77 | messagingSenderId: '54449445025' 78 | }; 79 | } 80 | 81 | return ENV; 82 | }; 83 | -------------------------------------------------------------------------------- /app/controllers/invoices/new.js: -------------------------------------------------------------------------------- 1 | import { next } from '@ember/runloop'; 2 | import Controller from '@ember/controller'; 3 | import NewController from 'fakturama/mixins/new-controller'; 4 | 5 | export default Controller.extend(NewController, { 6 | transitionTo() { 7 | this.transitionToRoute('invoice.show', this.get('content.model')); 8 | }, 9 | settings: null, 10 | currencies: null, 11 | taxRates: null, 12 | languages: null, 13 | units: null, 14 | clients: null, 15 | invoices: null, 16 | accounts: null, 17 | 18 | isRemoveItemDisabled: function() { 19 | return this.get('items.length') <= 1; 20 | }.property('items.@each'), 21 | 22 | contentDidChange: function() { 23 | let periodNumber, 24 | lastNumber, 25 | properties = {}; 26 | 27 | if (this.get('settings.numerationTypeCode') === 'year') { 28 | periodNumber = new Date().getFullYear().toString(); 29 | } 30 | 31 | if (this.get('settings.numerationTypeCode') === 'month') { 32 | periodNumber = 33 | (new Date().getMonth() + 1).toString() + 34 | '/' + 35 | new Date().getFullYear().toString(); 36 | } 37 | 38 | if (periodNumber) { 39 | lastNumber = 40 | this.get('invoices') 41 | .filterBy('periodNumber', periodNumber) 42 | .sortBy('periodicalNumber') 43 | .get('lastObject.periodicalNumber') || 0; 44 | properties.number = lastNumber + 1 + '/' + periodNumber; 45 | } 46 | 47 | properties.seller = this.get('settings.seller'); 48 | properties.sellerSignature = this.get('settings.contactName'); 49 | properties.dueDays = this.getWithDefault('settings.dueDays', 14); 50 | 51 | let model = this.get('model'); 52 | if (model) model.setProperties(properties); 53 | 54 | // bindings somehow don't work in minified version without Ember.run.next 55 | next(() => { 56 | let content = this.get('content'); 57 | if (content) content.addItem(); 58 | }); 59 | }.observes('content'), 60 | 61 | actions: { 62 | addItem() { 63 | this.get('model').addItem(); 64 | }, 65 | 66 | removeItem(item) { 67 | this.get('model.items').removeObject(item); 68 | }, 69 | 70 | chooseClient: function(client) { 71 | this.get('model').setProperties({ 72 | buyer: client.get('buyer'), 73 | buyerSignature: client.get('contactName') 74 | }); 75 | }, 76 | 77 | chooseAccount: function(account) { 78 | this.get('model').setProperties({ 79 | accountBankName: account.get('bankName'), 80 | accountSwift: account.get('swift'), 81 | accountNumber: account.get('number') 82 | }); 83 | } 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /app/templates/_client_form.hbs: -------------------------------------------------------------------------------- 1 |
2 | Firma 3 | 4 |
5 | 8 |
9 | {{input id="client-company-name" class="form-control" 10 | data-test-client-company-name=true value=(mut (get model 'companyName'))}} 11 |
12 |
13 | 14 | {{errors.companyName.firstObject}} 15 | 16 |
17 |
18 | 19 |
20 | 23 |
24 | {{textarea id="client-address" class="form-control" 25 | data-test-client-address=true value=(mut (get model 'address'))}} 26 |
27 |
28 | 29 | {{errors.address.firstObject}} 30 | 31 |
32 |
33 | 34 |
35 | 38 |
39 | {{input id="client-vatin" class="form-control" 40 | data-test-client-vatin=true value=(mut (get model 'vatin'))}} 41 |
42 |
43 | 44 | {{errors.vatin.firstObject}} 45 | 46 |
47 |
48 |
49 | 50 |
51 | Osoba kontaktowa 52 | 53 |
54 | 57 |
58 | {{input id="client-contact-name" class="form-control" 59 | data-test-client-contact-name=true value=(mut (get model 'contactName'))}} 60 |
61 |
62 | 63 | {{errors.contactName.firstObject}} 64 | 65 |
66 |
67 | 68 |
69 | 72 |
73 | {{input id="client-contact-email" class="form-control" type="email" 74 | data-test-client-contact-email=true value=(mut (get model 'contactEmail'))}} 75 |
76 |
77 | 78 | {{errors.contactEmail.firstObject}} 79 | 80 |
81 |
82 |
83 | -------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{#if showLayout}} 2 |
3 | 53 | 54 |
55 | {{outlet}} 56 |
57 |
58 |
59 | 60 | 69 | {{/if}} 70 | 71 | {{outlet 'no-layout' }} 72 | -------------------------------------------------------------------------------- /app/templates/settings.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | Dane osobiste 4 | 5 |
6 | 9 |
10 | {{input id="settings-contact-name" class="form-control" 11 | value=(mut (get model 'contactName'))}} 12 |
13 |
14 | 15 | {{errors.contactName.firstObject}} 16 | 17 |
18 |
19 |
20 | 21 |
22 | Firma 23 | 24 |
25 | 28 |
29 | {{input id="settings-company-name" class="form-control" 30 | value=(mut (get model 'companyName'))}} 31 |
32 |
33 | 34 | {{errors.companyName.firstObject}} 35 | 36 |
37 |
38 | 39 |
40 | 43 |
44 | {{textarea id="settings-address" class="form-control" 45 | value=(mut (get model 'address'))}} 46 |
47 |
48 | 49 | {{errors.address.firstObject}} 50 | 51 |
52 |
53 | 54 |
55 | 58 |
59 | {{input id="settings-vatin" class="form-control" 60 | value=(mut (get model 'vatin'))}} 61 |
62 |
63 | 64 | {{errors.vatin.firstObject}} 65 | 66 |
67 |
68 |
69 | 70 |
71 | Inne 72 | 73 |
74 | 77 |
78 | {{drop-down model=numerationTypes value=model.numerationTypeCode 79 | labelKey='name' valueKey='code' 80 | onSelect=(action (mut (get model 'numerationTypeCode')))}} 81 |
82 |
83 | 84 | {{errors.numerationTypeCode.firstObject}} 85 | 86 |
87 |
88 | 89 |
90 | 93 |
94 |
95 | {{input id="settings-due-days" class="form-control" type="number" min="0" 96 | value=(mut (get model 'dueDays'))}} 97 | dni 98 |
99 |
100 |
101 | 102 | {{errors.dueDays.firstObject}} 103 | 104 |
105 |
106 |
107 | 108 |
109 |
110 | 111 | {{link-to "Anuluj" "clients" class="btn btn-default" activeClass=null}} 112 |
113 |
114 | 115 |
116 | 117 |
118 | Baza danych 119 |
120 |
121 | {{export-button}} 122 | {{import-button import=(action 'importDatabase')}} 123 | 124 |
125 |
126 |
127 |
128 | 129 | {{#if isDeleteModalVisible}} 130 | 131 | 148 | {{/if}} 149 | -------------------------------------------------------------------------------- /app/styles/app.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,600&subset=latin,latin-ext"); 4 | 5 | // Core variables and mixins 6 | @import "bootstrap/less/variables.less"; 7 | 8 | @brand-primary: #1D4E6F; 9 | @font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | @font-size-base: 12px; 11 | 12 | @import "bootstrap/less/mixins.less"; 13 | 14 | // Reset 15 | @import "bootstrap/less/normalize.less"; 16 | @import "bootstrap/less/print.less"; 17 | 18 | // Core CSS 19 | @import "bootstrap/less/scaffolding.less"; 20 | @import "bootstrap/less/type.less"; 21 | @import "bootstrap/less/code.less"; 22 | @import "bootstrap/less/grid.less"; 23 | @import "bootstrap/less/tables.less"; 24 | @import "bootstrap/less/forms.less"; 25 | @import "bootstrap/less/buttons.less"; 26 | 27 | // Components 28 | @import "bootstrap/less/component-animations.less"; 29 | @import "bootstrap/less/glyphicons.less"; 30 | @import "bootstrap/less/dropdowns.less"; 31 | @import "bootstrap/less/button-groups.less"; 32 | @import "bootstrap/less/input-groups.less"; 33 | @import "bootstrap/less/navs.less"; 34 | @import "bootstrap/less/navbar.less"; 35 | @import "bootstrap/less/breadcrumbs.less"; 36 | @import "bootstrap/less/pagination.less"; 37 | @import "bootstrap/less/pager.less"; 38 | @import "bootstrap/less/labels.less"; 39 | @import "bootstrap/less/badges.less"; 40 | @import "bootstrap/less/jumbotron.less"; 41 | @import "bootstrap/less/thumbnails.less"; 42 | @import "bootstrap/less/alerts.less"; 43 | @import "bootstrap/less/progress-bars.less"; 44 | @import "bootstrap/less/media.less"; 45 | @import "bootstrap/less/list-group.less"; 46 | @import "bootstrap/less/panels.less"; 47 | @import "bootstrap/less/wells.less"; 48 | @import "bootstrap/less/close.less"; 49 | 50 | // Components w/ JavaScript 51 | @import "bootstrap/less/modals.less"; 52 | @import "bootstrap/less/tooltip.less"; 53 | @import "bootstrap/less/popovers.less"; 54 | @import "bootstrap/less/carousel.less"; 55 | 56 | // Utility classes 57 | @import "bootstrap/less/utilities.less"; 58 | @import "bootstrap/less/responsive-utilities.less"; 59 | 60 | .btn-add { 61 | .btn-default(); 62 | border: 1px dashed lightgray; 63 | } 64 | 65 | .btn-remove { 66 | .btn-default(); 67 | } 68 | 69 | .signature { 70 | border-bottom: 1px solid #ddd; 71 | } 72 | 73 | .gravatar { 74 | margin: -15px 5px; 75 | } 76 | 77 | .subtotal ~ .subtotal th { 78 | visibility: hidden; 79 | } 80 | 81 | html, 82 | body { 83 | height: 100%; 84 | 85 | & > .ember-view { 86 | height: 100vh; 87 | } 88 | } 89 | 90 | .wrap { 91 | min-height: 100%; 92 | height: auto !important; 93 | height: 100%; 94 | margin: 0 0 -50px; 95 | } 96 | 97 | .push, 98 | .site-footer { 99 | height: 50px; 100 | } 101 | 102 | .btn-upload { 103 | position: relative; 104 | overflow: hidden; 105 | 106 | input { 107 | position: absolute; 108 | top: 0; 109 | right: 0; 110 | margin: 0; 111 | opacity: 0; 112 | filter: alpha(opacity=0); 113 | transform: translate(0, 0) scale(4); 114 | font-size: 60px; 115 | width: 100%; 116 | direction: ltr; 117 | cursor: pointer; 118 | } 119 | } 120 | 121 | .site-content { 122 | margin-top: 30px; 123 | margin-bottom: 30px; 124 | } 125 | 126 | @media print { 127 | .site-content { 128 | margin-top: 0; 129 | margin-bottom: 0; 130 | } 131 | 132 | @page { 133 | @bottom-left { 134 | content: counter(page) "/" counter(pages); 135 | } 136 | } 137 | } 138 | 139 | // LANDING PAGE 140 | 141 | .site-header { 142 | color: white; 143 | background-image: url("/assets/binders.jpg"); 144 | background-repeat: no-repeat; 145 | -webkit-background-size: cover; 146 | -moz-background-size: cover; 147 | background-size: cover; 148 | background-position: center; 149 | width: 100%; 150 | 151 | strong { 152 | font-weight: 400; 153 | } 154 | 155 | svg path { 156 | fill: white; 157 | } 158 | 159 | h1 { 160 | font-size: 8em; 161 | margin: 50px 0; 162 | } 163 | 164 | h2 { 165 | margin: 50px 0; 166 | font-size: 3em; 167 | font-weight: 300; 168 | } 169 | 170 | h3 { 171 | margin: 20px 0; 172 | font-size: 2em; 173 | font-weight: 300; 174 | } 175 | } 176 | 177 | .site-title { 178 | font-size: 2.5em; 179 | font-weight: 300; 180 | } 181 | 182 | .header-title { 183 | padding: 75px 0; 184 | background: fade(@brand-primary, 75%); 185 | } 186 | 187 | .btn-issue { 188 | padding: 15px; 189 | font-size: 1.5em; 190 | color: @brand-primary; 191 | 192 | &:hover, &:active { 193 | color: @brand-primary; 194 | } 195 | } 196 | 197 | .header-marketing { 198 | padding: 75px 0; 199 | background-color: white; 200 | color: @gray-dark; 201 | 202 | p { 203 | font-weight: 300; 204 | font-size: 1.25em; 205 | color: @gray-darker; 206 | } 207 | 208 | svg path { 209 | fill: @brand-primary; 210 | } 211 | } 212 | 213 | .header-features { 214 | padding: 75px 0; 215 | background: @brand-primary; 216 | font-weight: 300; 217 | font-size: 1.25em; 218 | 219 | li { 220 | margin: 1em 0; 221 | } 222 | 223 | li:before { 224 | content: "✔"; 225 | font-size: 1.25em; 226 | margin: 0 0.5em; 227 | } 228 | } 229 | 230 | .site-footer { 231 | background: darken(@brand-primary, 15%); 232 | 233 | svg path { 234 | fill: white; 235 | } 236 | } 237 | 238 | .popover-source { 239 | display: block; 240 | position: relative; 241 | margin: 5px 10px; 242 | } 243 | 244 | @media (max-width: @screen-xs-max) { 245 | .site-header h1 { 246 | font-size: 4em; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /app/forms/invoice.js: -------------------------------------------------------------------------------- 1 | import { Promise as EmberPromise } from 'rsvp'; 2 | import { getOwner } from '@ember/application'; 3 | import ObjectProxy from '@ember/object/proxy'; 4 | import { computed } from '@ember/object'; 5 | import { inject as service } from '@ember/service'; 6 | import EmberValidations from 'ember-validations'; 7 | import ExchangeRateMixin from 'fakturama/mixins/exchange-rate'; 8 | import FormMixin from 'fakturama/mixins/form'; 9 | import InvoicePropertiesMixin from 'fakturama/mixins/invoice-properties'; 10 | import Item from 'fakturama/models/item'; 11 | import ItemForm from 'fakturama/forms/item'; 12 | 13 | const { oneWay } = computed; 14 | 15 | export default ObjectProxy.extend( 16 | EmberValidations, 17 | FormMixin, 18 | ExchangeRateMixin, 19 | InvoicePropertiesMixin, 20 | { 21 | store: service('store'), 22 | validations: { 23 | number: { 24 | presence: { 25 | if: 'isSubmitted', 26 | message: 'nie może być pusty' 27 | } 28 | }, 29 | issueDate: { 30 | presence: { 31 | if: 'isSubmitted', 32 | message: 'nie może być pusta' 33 | } 34 | }, 35 | deliveryDate: { 36 | presence: { 37 | if: 'isSubmitted', 38 | message: 'nie może być pusta' 39 | } 40 | }, 41 | dueDays: { 42 | presence: { 43 | if: 'isSubmitted', 44 | message: 'nie może być pusta' 45 | }, 46 | numericality: { 47 | if: 'isSubmitted', 48 | greaterThanOrEqualTo: 0, 49 | messages: { 50 | greaterThanOrEqualTo: 'nie może być ujemny' 51 | } 52 | } 53 | }, 54 | seller: { 55 | presence: { 56 | if: 'isSubmitted', 57 | message: 'nie może być pusty' 58 | } 59 | }, 60 | buyer: { 61 | presence: { 62 | if: 'isSubmitted', 63 | message: 'nie może być pusty' 64 | } 65 | }, 66 | currencyCode: { 67 | presence: { 68 | if: 'isSubmitted', 69 | message: 'nie może być pusta' 70 | } 71 | }, 72 | languageCode: { 73 | presence: { 74 | if: 'isSubmitted', 75 | message: 'nie może być pusta' 76 | } 77 | }, 78 | exchangeDate: { 79 | presence: { 80 | if: function(invoice) { 81 | return invoice.get('isSubmitted') && invoice.get('isExchanging'); 82 | }, 83 | message: 'nie może być pusta' 84 | } 85 | } 86 | }, 87 | 88 | id: oneWay('model.id'), 89 | number: oneWay('model.number'), 90 | issueDate: oneWay('model.issueDate'), 91 | deliveryDate: oneWay('model.deliveryDate'), 92 | seller: oneWay('model.seller'), 93 | buyer: oneWay('model.buyer'), 94 | currencyCode: oneWay('model.currencyCode'), 95 | languageCode: oneWay('model.languageCode'), 96 | accountBankName: oneWay('model.accountBankName'), 97 | accountSwift: oneWay('model.accountSwift'), 98 | accountNumber: oneWay('model.accountNumber'), 99 | isPaid: oneWay('model.isPaid'), 100 | 101 | items: computed( 102 | 'model.itemsAttributes', 103 | 'model.itemsAttributes.@each', 104 | function() { 105 | return this.getWithDefault('model.itemsAttributes', []).map( 106 | itemAttributes => { 107 | return ItemForm.create({ 108 | model: Item.create( 109 | Object.assign({}, itemAttributes, { container: getOwner(this) }) 110 | ), 111 | invoiceForm: this 112 | }); 113 | } 114 | ); 115 | } 116 | ), 117 | 118 | itemsAttributes: computed('items', 'items.@each', function() { 119 | return this.get('items').invoke('toJSON'); 120 | }), 121 | 122 | comment: oneWay('model.comment'), 123 | sellerSignature: oneWay('model.sellerSignature'), 124 | buyerSignature: oneWay('model.buyerSignature'), 125 | 126 | isSubmitted: false, 127 | isIssueDelivery: true, 128 | dueDays: 14, 129 | 130 | initIssueDate: function() { 131 | if (!this.get('issueDate')) { 132 | this.set('issueDate', new Date().toISOString().substr(0, 10)); 133 | } 134 | }.on('init'), 135 | 136 | initIsIssueDelivery: function() { 137 | this.set( 138 | 'isIssueDelivery', 139 | this.get('issueDate') === this.get('deliveryDate') 140 | ); 141 | }.on('init'), 142 | 143 | initDueDays: function() { 144 | var issueDate = Date.parse(this.get('issueDate')), 145 | dueDate = Date.parse(this.get('dueDate')); 146 | 147 | if (!isNaN(issueDate) && !isNaN(dueDate)) { 148 | this.set('dueDays', (dueDate - issueDate) / (1000 * 60 * 60 * 24)); 149 | } 150 | }.on('init'), 151 | 152 | isIssueDeliveryOrIssueDateDidChange: function() { 153 | if (this.get('isIssueDelivery')) { 154 | this.set('deliveryDate', this.get('issueDate')); 155 | } 156 | }.observes('isIssueDelivery', 'issueDate'), 157 | 158 | dueDaysOrIssueDateDidChange: function() { 159 | var date, 160 | dueDays = this.get('dueDays'), 161 | issueDate = this.get('issueDate'); 162 | 163 | date = Date.parse(issueDate) + 1000 * 60 * 60 * 24 * dueDays; 164 | 165 | if (!isNaN(date)) { 166 | this.set('dueDate', new Date(date).toISOString().substr(0, 10)); 167 | } 168 | }.observes('dueDays', 'issueDate'), 169 | 170 | addItem: function() { 171 | const item = ItemForm.create({ 172 | invoiceForm: this, 173 | model: Item.create({ 174 | quantity: 0, 175 | netPrice: 0, 176 | container: getOwner(this) 177 | }) 178 | }); 179 | this.get('items').pushObject(item); 180 | }, 181 | 182 | validate() { 183 | return EmberPromise.all( 184 | [this._super()].concat(this.get('items').invoke('validate')) 185 | ); 186 | }, 187 | 188 | currency: computed('currencyCode', function() { 189 | const code = this.get('currencyCode'); 190 | 191 | if (code) { 192 | return this.get('store').queryRecord('currency', { code }); 193 | } 194 | }), 195 | 196 | language: oneWay('languageCode', function() { 197 | const code = this.get('languageCode'); 198 | 199 | if (code) { 200 | return this.get('store').findRecord('language', code); 201 | } 202 | }) 203 | } 204 | ); 205 | -------------------------------------------------------------------------------- /app/mixins/invoice-properties.js: -------------------------------------------------------------------------------- 1 | import { all } from 'rsvp'; 2 | import { A } from '@ember/array'; 3 | import ArrayProxy from '@ember/array/proxy'; 4 | import { getOwner } from '@ember/application'; 5 | import Mixin from '@ember/object/mixin'; 6 | import EmberObject, { computed } from '@ember/object'; 7 | import Item from 'fakturama/models/item'; 8 | 9 | import polishToWords from 'polish-to-words'; 10 | 11 | export default Mixin.create({ 12 | sellerFirstLine: computed('seller', function() { 13 | return this.getWithDefault('seller', '').split('\n')[0]; 14 | }), 15 | 16 | sellerRest: computed('seller', function() { 17 | return this.getWithDefault('seller', '') 18 | .split('\n') 19 | .slice(1); 20 | }), 21 | 22 | buyerFirstLine: computed('buyer', function() { 23 | return this.getWithDefault('buyer', '').split('\n')[0]; 24 | }), 25 | 26 | buyerRest: computed('buyer', function() { 27 | return this.getWithDefault('buyer', '') 28 | .split('\n') 29 | .slice(1); 30 | }), 31 | 32 | commentLines: computed('comment', function() { 33 | return this.getWithDefault('comment', '').split('\n'); 34 | }), 35 | 36 | periodNumber: computed('number', function() { 37 | const number = this.getWithDefault('number', '').match(/([^/]+)\/(.+)/); 38 | return number ? number[2] : 1; 39 | }), 40 | 41 | periodicalNumber: computed('number', function() { 42 | const number = this.getWithDefault('number', '').match(/([^/]+)\/(.+)/); 43 | return number ? parseInt(number[1], 10) : 0; 44 | }), 45 | 46 | items: computed('itemsAttributes', 'itemsAttributes.@each', function() { 47 | return this.getWithDefault('itemsAttributes', []).map(itemAttributes => { 48 | return Item.create( 49 | Object.assign({}, itemAttributes, { container: getOwner(this) }) 50 | ); 51 | }); 52 | }), 53 | 54 | currency: computed('currencyCode', function() { 55 | var code = this.get('currencyCode'); 56 | if (code) { 57 | return this.get('store').queryRecord('currency', { code }); 58 | } 59 | }), 60 | 61 | language: computed('languageCode', function() { 62 | var code = this.get('languageCode'); 63 | if (code) { 64 | return this.get('store').queryRecord('language', { code }); 65 | } 66 | }), 67 | 68 | subTotals: computed( 69 | 'items', 70 | 'items.@each.netAmount', 71 | 'items.@each.taxAmount', 72 | 'items.@each.grossAmount', 73 | 'items.@each.taxRate', 74 | 'exchangeRate', 75 | 'exchangeDivisor', 76 | function() { 77 | let results = ArrayProxy.create({ content: A([]) }); 78 | 79 | all(this.get('items').map(item => item.get('taxRate'))).then(() => { 80 | this.get('items') 81 | .mapBy('taxRate') 82 | .uniq() 83 | .map(taxRate => { 84 | const items = this.get('items').filterBy('taxRate', taxRate); 85 | let result = EmberObject.create({ taxRate: taxRate }); 86 | 87 | result.set( 88 | 'netAmount', 89 | items.reduce(function(previousValue, item) { 90 | return previousValue + item.getWithDefault('netAmount', 0); 91 | }, 0) 92 | ); 93 | 94 | result.set( 95 | 'taxAmount', 96 | Math.round( 97 | (result.get('netAmount') * result.get('taxRate.value')) / 100 98 | ) 99 | ); 100 | result.set( 101 | 'grossAmount', 102 | result.get('netAmount') + result.get('taxAmount') 103 | ); 104 | 105 | if (this.get('exchangeRate')) { 106 | result.set( 107 | 'taxAmountPLN', 108 | Math.round( 109 | (result.get('taxAmount') * this.get('exchangeRate')) / 110 | (this.get('exchangeDivisor') * 10000) 111 | ) 112 | ); 113 | } 114 | 115 | results.pushObject(result); 116 | }); 117 | }); 118 | 119 | return results; 120 | } 121 | ), 122 | 123 | totalNetAmount: computed( 124 | 'subTotals', 125 | 'subTotals.@each.netAmount', 126 | function() { 127 | return this.get('subTotals').reduce(function(previousValue, item) { 128 | return previousValue + item.get('netAmount'); 129 | }, 0); 130 | } 131 | ), 132 | 133 | totalTaxAmount: computed( 134 | 'subTotals', 135 | 'subTotals.@each.taxAmount', 136 | function() { 137 | return this.get('subTotals').reduce(function(previousValue, item) { 138 | return previousValue + item.getWithDefault('taxAmount', 0); 139 | }, 0); 140 | } 141 | ), 142 | 143 | totalGrossAmount: computed( 144 | 'subTotals', 145 | 'subTotals.@each.grossAmount', 146 | function() { 147 | return this.get('subTotals').reduce(function(previousValue, item) { 148 | return previousValue + item.get('grossAmount'); 149 | }, 0); 150 | } 151 | ), 152 | 153 | totalTaxAmountPLN: computed( 154 | 'totalTaxAmount', 155 | 'exchangeRate', 156 | 'exchangeDivisor', 157 | 'isExchanging', 158 | function() { 159 | if (this.get('isExchanging')) { 160 | return Math.round( 161 | (this.get('totalTaxAmount') * this.get('exchangeRate')) / 162 | (this.get('exchangeDivisor') * 10000) 163 | ); 164 | } 165 | } 166 | ), 167 | 168 | totalGrossAmountPLN: computed( 169 | 'totalGrossAmount', 170 | 'exchangeRate', 171 | 'exchangeDivisor', 172 | 'isForeignCurrency', 173 | function() { 174 | if (this.get('isForeignCurrency')) { 175 | return Math.round( 176 | (this.get('totalGrossAmount') * this.get('exchangeRate')) / 177 | (this.get('exchangeDivisor') * 10000) 178 | ); 179 | } else { 180 | return this.get('totalGrossAmount'); 181 | } 182 | } 183 | ), 184 | 185 | totalGrossAmountInWords: computed( 186 | 'totalGrossAmount', 187 | 'currency.code', 188 | function() { 189 | const amount = String(this.get('totalGrossAmount')); 190 | const dollars = amount.substr(0, amount.length - 2) || '0'; 191 | const cents = amount.substr(amount.length - 2, amount.length) || '0'; 192 | 193 | return `${polishToWords(dollars)} ${this.get( 194 | 'currency.code' 195 | )} ${cents}/100`; 196 | } 197 | ), 198 | 199 | englishTotalGrossAmountInWords: computed( 200 | 'totalGrossAmount', 201 | 'currency.code', 202 | function() { 203 | var dollars, 204 | cents, 205 | amount = String(this.get('totalGrossAmount')); 206 | 207 | dollars = amount.substr(0, amount.length - 2) || '0'; 208 | cents = amount.substr(amount.length - 2, amount.length) || '0'; 209 | 210 | return ( 211 | window.toWords(dollars) + 212 | ' ' + 213 | this.get('currency.code') + 214 | ' ' + 215 | cents + 216 | '/100' 217 | ); 218 | } 219 | ), 220 | 221 | isEnglish: computed('languageCode', function() { 222 | return this.get('languageCode') === 'plen'; 223 | }), 224 | 225 | isForeignCurrency: computed('currencyCode', function() { 226 | return this.get('currencyCode') !== 'PLN'; 227 | }), 228 | 229 | isExchanging: computed( 230 | 'currencyCode', 231 | 'issueDate', 232 | 'totalTaxAmount', 233 | 'isForeignCurrency', 234 | function() { 235 | return ( 236 | !!this.get('currencyCode') && 237 | !!this.get('issueDate') && 238 | !!this.get('totalTaxAmount') && 239 | this.get('isForeignCurrency') 240 | ); 241 | } 242 | ), 243 | 244 | isExpired: computed('dueDate', function() { 245 | return Date.parse(this.get('dueDate')) < new Date().getTime(); 246 | }), 247 | 248 | isOverdue: computed('isExpired', 'isPaid', function() { 249 | return this.get('isExpired') && !this.get('isPaid'); 250 | }) 251 | }); 252 | -------------------------------------------------------------------------------- /app/templates/invoice/show.hbs: -------------------------------------------------------------------------------- 1 |

2 | 3 | Faktura 4 | {{#if model.isEnglish}} 5 | / Invoice 6 | {{/if}} 7 | 8 |

9 | 10 |
11 | 12 |
13 |

14 | {{#if model.isEnglish}} 15 | Numer / Number: 16 | {{else}} 17 | Numer: 18 | {{/if}} 19 | {{model.number}} 20 |

21 |

22 | {{#if model.isEnglish}} 23 | Data wystawienia / Date of issue: 24 | {{else}} 25 | Data wystawienia: 26 | {{/if}} 27 | {{model.issueDate}} 28 |

29 |

30 | {{#if model.isEnglish}} 31 | Data dostawy towarów/wykonania usługi / Delivery date of goods/services: 32 | {{else}} 33 | Data dostawy towarów/wykonania usługi: 34 | {{/if}} 35 | {{model.deliveryDate}} 36 |

37 |

38 | {{#if model.isEnglish}} 39 | Termin płatności / Due date: 40 | {{else}} 41 | Termin płatności: 42 | {{/if}} 43 | {{model.dueDate}} 44 |

45 | 46 | {{#if model.isExchanging}} 47 |

48 | Kwoty podatku VAT zostały przeliczone po kursie średnim {{model.currencyCode}} z dnia {{model.exchangeDate}}, 49 | wynoszącym {{format-cents exchangeRate precision=4}} PLN. 50 |

51 | {{/if}} 52 |
53 | 54 |
55 |
56 |

57 | 58 | Sprzedawca 59 | {{#if model.isEnglish}} 60 | / Seller 61 | {{/if}} 62 | 63 |

64 |

65 | {{model.sellerFirstLine}}
66 | {{#each model.sellerRest as |line|}} 67 | {{line}}
68 | {{/each}} 69 |

70 |
71 | 72 |
73 |

74 | 75 | Nabywca 76 | {{#if model.isEnglish}} 77 | / Buyer 78 | {{/if}} 79 | 80 |

81 |

82 | {{model.buyerFirstLine}}
83 | {{#each model.buyerRest as |line|}} 84 | {{line}}
85 | {{/each}} 86 |

87 |
88 |
89 | 90 |
91 | 92 | 93 | 94 | 95 | 98 | 101 | 104 | 107 | 110 | 113 | 116 | 119 | 120 | 121 | {{#if model.isEnglish}} 122 | 123 | 126 | 129 | 132 | 135 | 138 | 141 | 144 | 147 | 148 | {{/if}} 149 | 150 | 151 | 152 | {{#each model.items as |item|}} 153 | 154 | 159 | 162 | 170 | 176 | 182 | 190 | 196 | 202 | 203 | {{/each}} 204 | 205 | 211 | 215 | 216 | 224 | 228 | 229 | {{#each subTotals as |subTotal|}} 230 | 231 | 237 | 241 | 247 | 255 | 259 | 260 | {{/each}} 261 | 262 |
96 | Nazwa 97 | 99 | Liczba 100 | 102 | J.m. 103 | 105 | Cena netto 106 | 108 | Wartość netto 109 | 111 | Stawka VAT 112 | 114 | Wartość VAT 115 | 117 | Wartość brutto 118 |
124 | Name 125 | 127 | Quantity 128 | 130 | Unit 131 | 133 | Net price 134 | 136 | Net amount 137 | 139 | VAT rate 140 | 142 | VAT amount 143 | 145 | Gross amount 146 |
155 |

156 | {{item.description}} 157 |

158 |
160 |

{{item.quantity}}

161 |
163 |

164 | {{item.unit.name}} 165 | {{#if model.isEnglish}} 166 |
{{item.unit.nameEN}} 167 | {{/if}} 168 |

169 |
171 |

172 | {{format-cents item.netPrice}} 173 | {{model.currency.code}} 174 |

175 |
177 |

178 | {{format-cents item.netAmount}} 179 | {{model.currency.code}} 180 |

181 |
183 |

184 | {{item.taxRate.name}} 185 | {{#if model.isEnglish}} 186 |
{{item.taxRate.nameEN}} 187 | {{/if}} 188 |

189 |
191 |

192 | {{format-cents item.taxAmount}} 193 | {{model.currency.code}} 194 |

195 |
197 |

198 | {{format-cents item.grossAmount}} 199 | {{model.currency.code}} 200 |

201 |
206 | Razem 207 | {{#if model.isEnglish}} 208 | / Total 209 | {{/if}} 210 | 212 | {{format-cents model.totalNetAmount}} 213 | {{model.currency.code}} 214 | 217 | {{format-cents model.totalTaxAmount}} 218 | {{model.currency.code}} 219 | {{#if model.isExchanging}} 220 |
{{format-cents model.totalTaxAmountPLN}} 221 | PLN 222 | {{/if}} 223 |
225 | {{format-cents model.totalGrossAmount}} 226 | {{model.currency.code}} 227 |
232 | W tym 233 | {{#if model.isEnglish}} 234 | / Including 235 | {{/if}} 236 | 238 | {{format-cents subTotal.netAmount}} 239 | {{model.currency.code}} 240 | 242 | {{subTotal.taxRate.name}} 243 | {{#if isEnglish}} 244 |
{{subTotal.taxRate.nameEN}} 245 | {{/if}} 246 |
248 | {{format-cents subTotal.taxAmount}} 249 | {{currency.code}} 250 | {{#if model.isExchanging}} 251 |
{{format-cents subTotal.taxAmountPLN}} 252 | PLN 253 | {{/if}} 254 |
256 | {{format-cents subTotal.grossAmount}} 257 | {{currency.code}} 258 |
263 | 264 |
265 |
266 |

267 | Słownie: {{model.totalGrossAmountInWords}} 268 |

269 | {{#if model.isEnglish}} 270 |

271 | In words: {{model.englishTotalGrossAmountInWords}} 272 |

273 | {{/if}} 274 |
275 |
276 | 277 | {{#if model.accountNumber}} 278 |
279 |
280 |

281 | 282 | Płatność 283 | {{#if model.isEnglish}} 284 | / Payment 285 | {{/if}} 286 | 287 |

288 | {{#if model.accountBankName}} 289 |

290 | {{model.accountBankName}} 291 |

292 | {{/if}} 293 | {{#if model.accountSwift}} 294 |

295 | SWIFT: {{model.accountSwift}} 296 |

297 | {{/if}} 298 |

299 | IBAN: {{model.accountNumber}} 300 |

301 |
302 |
303 | {{/if}} 304 | 305 | {{#if model.comment}} 306 |
307 |
308 |

309 | 310 | Uwagi 311 | {{#if model.isEnglish}} 312 | / Comments 313 | {{/if}} 314 | 315 |

316 |

317 | {{#each model.commentLines as |line|}} 318 | {{line}}
319 | {{/each}} 320 |

321 |
322 |
323 | {{/if}} 324 | 325 |
326 | 327 |
328 |
329 | {{#if model.sellerSignature}} 330 |

{{model.sellerSignature}}

331 |

332 | Podpis wystawiającego 333 | {{#if model.isEnglish}} 334 | / Issuer's signature 335 | {{/if}} 336 |

337 | {{/if}} 338 |
339 |
340 | {{#if model.buyerSignature}} 341 |

{{model.buyerSignature}}

342 |

343 | Podpis nabywcy 344 | {{#if model.isEnglish}} 345 | / Buyer's signature 346 | {{/if}} 347 |

348 | {{/if}} 349 |
350 |
351 | 352 |
353 |
354 | Drukuj fakturę 355 | {{link-to "Edytuj fakturę" "invoice.edit" model class="btn btn-warning"}} 356 | {{link-to "Anuluj" "invoices" class="btn btn-default" active=null}} 357 |
358 |
359 |
360 | -------------------------------------------------------------------------------- /app/templates/home.hbs: -------------------------------------------------------------------------------- 1 |
2 | 98 |
99 |
100 | 101 | 115 | -------------------------------------------------------------------------------- /app/templates/_invoice_form.hbs: -------------------------------------------------------------------------------- 1 |
2 | Faktura 3 | 4 |
5 | 8 |
9 | {{input id="invoice-number" class="form-control" 10 | value=(mut (get model 'number'))}} 11 |
12 |
13 | 14 | {{errors.number.firstObject}} 15 | 16 |
17 |
18 | 19 |
20 | 23 |
24 | {{input id="invoice-issue-date" class="form-control" type="date" 25 | value=(mut (get model 'issueDate'))}} 26 |
27 |
28 | 29 | {{errors.issueDate.firstObject}} 30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 | 41 |
42 |
43 |
44 | 45 |
46 | 49 |
50 | {{input id="invoice-delivery-date" class="form-control" type="date" 51 | value=(mut (get model 'deliveryDate'))}} 52 |
53 |
54 | 55 | {{errors.deliveryDate.firstObject}} 56 | 57 |
58 |
59 | 60 |
61 | 64 |
65 |
66 | {{input id="invoice-due-days" class="form-control" type="number" min="0" 67 | value=(mut (get model 'dueDays'))}} 68 | dni 69 |
70 |
71 |
72 | 73 | {{errors.dueDays.firstObject}} 74 | 75 |
76 |
77 | 78 |
79 | 82 |
83 | {{drop-down model=currencies value=model.currencyCode 84 | labelKey='nameWithCode' valueKey='code' 85 | onSelect=(action (mut (get model 'currencyCode')))}} 86 |
87 |
88 | {{errors.currency.firstObject}} 89 |
90 |
91 | 92 | {{#if model.isExchanging}} 93 |
94 | 97 |
98 | {{input id="invoice-exchange-date" class="form-control" type="date" disabled="true" 99 | value=(get model 'exchangeDate')}} 100 |
101 |
102 | {{#if model.exchangeRate.isLoading}} 103 | 104 | Trwa ładowanie tabeli kursów walut obcych, proszę czekać… 105 | 106 | {{/if}} 107 |
108 |
109 | {{/if}} 110 | 111 |
112 | 115 |
116 | {{drop-down model=languages value=model.languageCode 117 | labelKey='name' valueKey='code' 118 | onSelect=(action (mut (get model 'languageCode')))}} 119 |
120 |
121 | 122 | {{errors.language.firstObject}} 123 | 124 |
125 |
126 | 127 |
128 |
129 | 132 | 133 | {{errors.seller.firstObject}} 134 | 135 | {{textarea id="invoice-sellet" class="form-control" rows="5" 136 | value=(mut (get model 'seller'))}} 137 |
138 | 139 |
140 | 161 | {{errors.buyer.firstObject}} 162 | {{textarea id="invoice-buyer" class="form-control" rows="5" 163 | value=(mut (get model 'buyer'))}} 164 |
165 |
166 | 167 |
168 | 169 | 170 | 171 | 172 | 175 | 178 | 181 | 184 | 187 | 190 | 193 | 196 | 197 | 198 | {{#each model.items as |item|}} 199 | 200 | 203 | 207 | 212 | 216 | 220 | 225 | 229 | 233 | 238 | 239 | {{/each}} 240 | 241 | 242 | 243 | 244 | 247 | 251 | 252 | 261 | 265 | 266 | 267 | {{#each model.subTotals as |subTotal|}} 268 | 269 | 272 | 275 | 278 | 286 | 289 | 290 | 291 | {{/each}} 292 | 293 |
173 | Nazwa 174 | 176 | Liczba 177 | 179 | J.m. 180 | 182 | Cena netto 183 | 188 | Stawka VAT 189 |
201 | {{input class="form-control" value=(mut (get item 'description'))}} 202 | 204 | {{input class="form-control text-right" type="number" 205 | value=(mut (get item 'quantity'))}} 206 | 208 | {{drop-down model=units value=item.unitCode 209 | labelKey='name' valueKey='code' 210 | onSelect=(action (mut (get item 'unitCode')))}} 211 | 213 | {{cents-field class="form-control text-right" cents=(mut (get item 'netPrice')) 214 | precision=(get model.currency 'precision')}} 215 | 221 | {{drop-down model=taxRates value=item.taxRateCode 222 | labelKey='name' valueKey='code' 223 | onSelect=(action (mut (get item 'taxRateCode')))}} 224 | 234 | 237 |
294 | 295 |
296 |
297 |

Słownie: {{model.totalGrossAmountInWords}}

298 |
299 |
300 | 301 |
302 |
303 | 324 |
325 |
326 | 327 |
328 | 331 |
332 | {{input id="invoice-account-bank-name" class="form-control" 333 | value=(mut (get model 'accountBankName'))}} 334 |
335 |
336 | 337 | {{errors.accountBankName.firstObject}} 338 | 339 |
340 |
341 | 342 |
343 | 346 |
347 | {{input id="invoice-account-swift" class="form-control" 348 | value=(mut (get model 'accountSwift'))}} 349 |
350 |
351 | 352 | {{errors.accountSwift.firstObject}} 353 | 354 |
355 |
356 | 357 |
358 | 361 |
362 | {{input id="invoice-account-number" class="form-control" 363 | value=(mut (get model 'accountNumber'))}} 364 |
365 |
366 | 367 | {{errors.accountNumber.firstObject}} 368 | 369 |
370 |
371 | 372 |
373 |
374 | 377 | {{textarea id="commment" class="form-control" rows="4" value=(mut (get model 'comment'))}} 378 |
379 |
380 | 381 |
382 |
383 | {{input id="sellerSignature" class="form-control text-center" 384 | value=(mut (get model 'sellerSignature'))}} 385 | 386 |
387 |
388 | {{input id="buyerSignature" class="form-control text-center" 389 | value=(mut (get model 'buyerSignature'))}} 390 | 391 |
392 |
393 |
394 | --------------------------------------------------------------------------------