harvested 0.6.2 → 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -3
- data/HISTORY +30 -26
- data/README.md +3 -3
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/examples/project_create_script.rb +93 -0
- data/harvested.gemspec +18 -3
- data/lib/harvest/api/invoice_categories.rb +5 -5
- data/lib/harvest/api/invoice_payments.rb +31 -0
- data/lib/harvest/api/invoices.rb +27 -8
- data/lib/harvest/api/tasks.rb +27 -0
- data/lib/harvest/base.rb +78 -56
- data/lib/harvest/behavior/crud.rb +2 -1
- data/lib/harvest/expense.rb +2 -2
- data/lib/harvest/invoice.rb +56 -20
- data/lib/harvest/invoice_payment.rb +8 -0
- data/lib/harvest/time_entry.rb +2 -2
- data/lib/harvested.rb +6 -6
- data/spec/factories.rb +57 -0
- data/spec/functional/clients_spec.rb +10 -16
- data/spec/functional/invoice_payments_spec.rb +44 -0
- data/spec/functional/invoice_spec.rb +88 -40
- data/spec/functional/project_spec.rb +2 -8
- data/spec/functional/tasks_spec.rb +2 -8
- data/spec/harvest/expense_spec.rb +2 -2
- data/spec/harvest/invoice_payment_spec.rb +5 -0
- data/spec/harvest/invoice_spec.rb +9 -10
- data/spec/harvest/time_entry_spec.rb +3 -3
- data/spec/spec_helper.rb +3 -0
- data/spec/support/harvested_helpers.rb +1 -1
- metadata +58 -4
data/lib/harvested.rb
CHANGED
@@ -19,14 +19,14 @@ require 'harvest/timezones'
|
|
19
19
|
require 'harvest/base'
|
20
20
|
|
21
21
|
%w(crud activatable).each {|a| require "harvest/behavior/#{a}"}
|
22
|
-
%w(model client contact project task user rate_limit_status task_assignment user_assignment expense_category expense time_entry invoice_category line_item invoice).each {|a| require "harvest/#{a}"}
|
23
|
-
%w(base account clients contacts projects tasks users task_assignments user_assignments expense_categories expenses time reports invoice_categories invoices).each {|a| require "harvest/api/#{a}"}
|
22
|
+
%w(model client contact project task user rate_limit_status task_assignment user_assignment expense_category expense time_entry invoice_category line_item invoice invoice_payment).each {|a| require "harvest/#{a}"}
|
23
|
+
%w(base account clients contacts projects tasks users task_assignments user_assignments expense_categories expenses time reports invoice_categories invoices invoice_payments).each {|a| require "harvest/api/#{a}"}
|
24
24
|
|
25
25
|
module Harvest
|
26
26
|
VERSION = File.read(File.expand_path(File.join(File.dirname(__FILE__), '..', 'VERSION'))).strip
|
27
|
-
|
27
|
+
|
28
28
|
class << self
|
29
|
-
|
29
|
+
|
30
30
|
# Creates a standard client that will raise all errors it encounters
|
31
31
|
#
|
32
32
|
# == Options
|
@@ -38,7 +38,7 @@ module Harvest
|
|
38
38
|
def client(subdomain, username, password, options = {})
|
39
39
|
Harvest::Base.new(subdomain, username, password, options)
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
# Creates a hardy client that will retry common HTTP errors it encounters and sleep() if it determines it is over your rate limit
|
43
43
|
#
|
44
44
|
# == Options
|
@@ -66,4 +66,4 @@ module Harvest
|
|
66
66
|
Harvest::HardyClient.new(client(subdomain, username, password, options), (retries || 5))
|
67
67
|
end
|
68
68
|
end
|
69
|
-
end
|
69
|
+
end
|
data/spec/factories.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
sequence :name do |n|
|
3
|
+
"Joe's Steam Cleaning #{n}"
|
4
|
+
end
|
5
|
+
|
6
|
+
sequence :description do |n|
|
7
|
+
"Item #{n}"
|
8
|
+
end
|
9
|
+
|
10
|
+
sequence :project_name do |n|
|
11
|
+
"Joe's Steam Cleaning Project #{n}"
|
12
|
+
end
|
13
|
+
|
14
|
+
factory :client, class: Harvest::Client do
|
15
|
+
name
|
16
|
+
details "Steam Cleaning across the country"
|
17
|
+
end
|
18
|
+
|
19
|
+
factory :invoice, class: Harvest::Invoice do
|
20
|
+
subject "Invoice for Joe's Stream Cleaning"
|
21
|
+
client_id nil
|
22
|
+
issued_at "2011-03-31"
|
23
|
+
due_at "2011-03-31"
|
24
|
+
due_at_human_format "upon receipt"
|
25
|
+
|
26
|
+
currency "United States Dollars - USD"
|
27
|
+
sequence(:number)
|
28
|
+
notes "Some notes go here"
|
29
|
+
period_end "2011-03-31"
|
30
|
+
period_start "2011-02-26"
|
31
|
+
kind "free_form"
|
32
|
+
state "draft"
|
33
|
+
purchase_order nil
|
34
|
+
tax nil
|
35
|
+
tax2 nil
|
36
|
+
import_hours "no"
|
37
|
+
import_expenses "no"
|
38
|
+
line_items { [FactoryGirl.build(:line_item)] }
|
39
|
+
end
|
40
|
+
|
41
|
+
factory :line_item, class: Harvest::LineItem do
|
42
|
+
kind "Service"
|
43
|
+
description
|
44
|
+
quantity 200
|
45
|
+
unit_price "12.00"
|
46
|
+
end
|
47
|
+
|
48
|
+
factory :invoice_payment, class: Harvest::InvoicePayment do
|
49
|
+
paid_at Time.now
|
50
|
+
amount "0.00"
|
51
|
+
notes "Payment received"
|
52
|
+
end
|
53
|
+
|
54
|
+
factory :project, class: Harvest::Project do
|
55
|
+
name { generate(:project_name) }
|
56
|
+
end
|
57
|
+
end
|
@@ -2,12 +2,11 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe 'harvest clients' do
|
4
4
|
it 'allows adding, updating and removing clients' do
|
5
|
+
client_attributes = FactoryGirl.attributes_for(:client)
|
6
|
+
|
5
7
|
cassette("client") do
|
6
|
-
client
|
7
|
-
|
8
|
-
"details" => "Building API Widgets across the country"
|
9
|
-
)
|
10
|
-
client.name.should == "Joe's Steam Cleaning"
|
8
|
+
client = harvest.clients.create(client_attributes)
|
9
|
+
client.name.should == client_attributes[:name]
|
11
10
|
|
12
11
|
client.name = "Joe and Frank's Steam Cleaning"
|
13
12
|
client = harvest.clients.update(client)
|
@@ -20,10 +19,7 @@ describe 'harvest clients' do
|
|
20
19
|
|
21
20
|
it 'allows activating and deactivating clients' do
|
22
21
|
cassette("client2") do
|
23
|
-
client
|
24
|
-
"name" => "Joe's Steam Cleaning",
|
25
|
-
"details" => "Building API Widgets across the country"
|
26
|
-
)
|
22
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
27
23
|
client.should be_active
|
28
24
|
|
29
25
|
client = harvest.clients.deactivate(client)
|
@@ -37,21 +33,19 @@ describe 'harvest clients' do
|
|
37
33
|
context "contacts" do
|
38
34
|
it "allows adding, updating, and removing contacts" do
|
39
35
|
cassette("client3") do
|
40
|
-
client
|
41
|
-
|
42
|
-
"details" => "Building API Widgets across the country"
|
43
|
-
)
|
36
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
37
|
+
|
44
38
|
contact = harvest.contacts.create(
|
45
39
|
"client_id" => client.id,
|
46
|
-
"email" => "jane@
|
40
|
+
"email" => "jane@example.com",
|
47
41
|
"first_name" => "Jane",
|
48
42
|
"last_name" => "Doe"
|
49
43
|
)
|
50
44
|
contact.client_id.should == client.id
|
51
|
-
contact.email.should == "jane@
|
45
|
+
contact.email.should == "jane@example.com"
|
52
46
|
|
53
47
|
harvest.contacts.delete(contact)
|
54
|
-
harvest.contacts.all.select {|e| e.email == "jane@
|
48
|
+
harvest.contacts.all.select {|e| e.email == "jane@example.com" }.should == []
|
55
49
|
end
|
56
50
|
end
|
57
51
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'harvest invoice payments' do
|
4
|
+
it 'allows retrieving existing invoice payments' do
|
5
|
+
cassette('invoice_payment1') do
|
6
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
7
|
+
invoice = harvest.invoices.create(FactoryGirl.attributes_for(:invoice, :client_id => client.id))
|
8
|
+
|
9
|
+
payment = Harvest::InvoicePayment.new(FactoryGirl.attributes_for(:invoice_payment, :invoice_id => invoice.id, :amount => invoice.amount))
|
10
|
+
payment_saved = harvest.invoice_payments.create(payment)
|
11
|
+
|
12
|
+
payment_found = harvest.invoice_payments.find(invoice, payment_saved)
|
13
|
+
|
14
|
+
payment_found.should == payment_saved
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'allows adding, and removing invoice payments' do
|
19
|
+
cassette('invoice_payment2') do
|
20
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
21
|
+
invoice = harvest.invoices.create(FactoryGirl.attributes_for(:invoice, :client_id => client.id))
|
22
|
+
|
23
|
+
half_amount = (invoice.amount.to_f / 2)
|
24
|
+
|
25
|
+
payment1 = Harvest::InvoicePayment.new(FactoryGirl.attributes_for(:invoice_payment, :invoice_id => invoice.id, :amount => half_amount))
|
26
|
+
payment1 = harvest.invoice_payments.create(payment1)
|
27
|
+
|
28
|
+
invoice = harvest.invoices.find(invoice.id)
|
29
|
+
invoice.state.should == 'partial'
|
30
|
+
|
31
|
+
payment2 = Harvest::InvoicePayment.new(FactoryGirl.attributes_for(:invoice_payment, :invoice_id => invoice.id, :amount => half_amount))
|
32
|
+
payment2 = harvest.invoice_payments.create(payment2)
|
33
|
+
|
34
|
+
invoice = harvest.invoices.find(invoice.id)
|
35
|
+
invoice.state.should == 'paid'
|
36
|
+
|
37
|
+
harvest.invoice_payments.all(invoice).each { |ip| harvest.invoice_payments.delete(ip) }
|
38
|
+
harvest.invoice_payments.all(invoice).should be_empty
|
39
|
+
|
40
|
+
invoice = harvest.invoices.find(invoice.id)
|
41
|
+
invoice.state.should == 'open'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -5,61 +5,109 @@ describe 'harvest invoices' do
|
|
5
5
|
cassette('invoice1') do
|
6
6
|
cat = harvest.invoice_categories.create("name" => "New Category")
|
7
7
|
cat.name.should == "New Category"
|
8
|
-
|
8
|
+
|
9
9
|
cat.name = "Updated Category"
|
10
10
|
cat = harvest.invoice_categories.update(cat)
|
11
11
|
cat.name.should == "Updated Category"
|
12
|
-
|
12
|
+
|
13
13
|
harvest.invoice_categories.delete(cat)
|
14
14
|
harvest.invoice_categories.all.select {|c| c.name == "Updated Category" }.should == []
|
15
15
|
end
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
it 'allows adding, updating and removing invoices' do
|
19
|
-
pending "having trouble following the API docs at http://www.getharvest.com/api/invoices"
|
20
|
-
|
21
19
|
cassette('invoice2') do
|
22
|
-
client = harvest.clients.create(
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
21
|
+
|
22
|
+
invoice = Harvest::Invoice.new(FactoryGirl.attributes_for(:invoice, :client_id => client.id))
|
23
|
+
invoice = harvest.invoices.create(invoice)
|
24
|
+
|
25
|
+
invoice.subject.should == "Invoice for Joe's Stream Cleaning"
|
26
|
+
invoice.amount.should == "2400.0"
|
27
|
+
invoice.line_items.size.should == 1
|
28
|
+
|
29
|
+
invoice.subject = "Updated Invoice for Joe"
|
30
|
+
invoice.line_items << FactoryGirl.build(:line_item)
|
31
|
+
|
32
|
+
invoice = harvest.invoices.update(invoice)
|
33
|
+
invoice.subject.should == "Updated Invoice for Joe"
|
34
|
+
invoice.amount.should == "4800.0"
|
35
|
+
invoice.line_items.size.should == 2
|
36
|
+
|
37
|
+
harvest.invoices.delete(invoice)
|
38
|
+
harvest.invoices.all.select {|p| p.number == "1000"}.should == []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'allows finding one invoice or all invoices with parameters' do
|
43
|
+
cassette('invoice3') do
|
44
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
45
|
+
|
46
|
+
project_attributes = FactoryGirl.attributes_for(:project)
|
47
|
+
project_attributes[:client_id] = client.id
|
48
|
+
|
49
|
+
project = harvest.projects.create(project_attributes)
|
50
|
+
project.name.should == project_attributes[:name]
|
51
|
+
|
52
|
+
# Delete any existing invoices.
|
53
|
+
harvest.invoices.all.each {|i| harvest.invoices.delete(i.id)}
|
54
|
+
|
26
55
|
invoice = Harvest::Invoice.new(
|
27
|
-
"subject"
|
28
|
-
"client_id"
|
29
|
-
"issued_at"
|
30
|
-
"due_at"
|
31
|
-
"
|
32
|
-
|
33
|
-
"
|
34
|
-
"
|
35
|
-
"
|
36
|
-
"
|
37
|
-
"
|
38
|
-
"
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
# "import_expenses" => "no"
|
46
|
-
# "line_items" => [Harvest::LineItem.new("kind" => "Service", "description" => "One item", "quantity" => 200, "unit_price" => "12.00")]
|
56
|
+
"subject" => "Invoice for Frannie's Widgets",
|
57
|
+
"client_id" => client.id,
|
58
|
+
"issued_at" => "2011-03-31",
|
59
|
+
"due_at" => "2011-05-31",
|
60
|
+
"currency" => "United States Dollars - USD",
|
61
|
+
"number" => 1000,
|
62
|
+
"notes" => "Some notes go here",
|
63
|
+
"period_end" => "2011-03-31",
|
64
|
+
"period_start" => "2011-02-26",
|
65
|
+
"kind" => "free_form",
|
66
|
+
"state" => "draft",
|
67
|
+
"purchase_order" => nil,
|
68
|
+
"tax" => nil,
|
69
|
+
"tax2" => nil,
|
70
|
+
"kind" => "free_form",
|
71
|
+
"import_hours" => "no",
|
72
|
+
"import_expenses" => "no",
|
73
|
+
"line_items" => [Harvest::LineItem.new("kind" => "Service", "description" => "One item", "quantity" => 200, "unit_price" => "12.00")]
|
47
74
|
)
|
48
|
-
p invoice.to_json
|
49
75
|
invoice = harvest.invoices.create(invoice)
|
50
|
-
|
76
|
+
|
51
77
|
invoice.subject.should == "Invoice for Frannie's Widgets"
|
52
78
|
invoice.amount.should == "2400.0"
|
53
79
|
invoice.line_items.size.should == 1
|
54
|
-
|
55
|
-
|
56
|
-
invoice
|
57
|
-
|
58
|
-
invoice
|
59
|
-
invoice.
|
60
|
-
|
61
|
-
|
62
|
-
|
80
|
+
invoice.due_at.should == "2011-05-31"
|
81
|
+
|
82
|
+
invoice = harvest.invoices.find(invoice.id)
|
83
|
+
invoice.subject.should == "Invoice for Frannie's Widgets"
|
84
|
+
invoice.amount.should == "2400.0"
|
85
|
+
invoice.line_items.size.should == 1
|
86
|
+
|
87
|
+
invoices = harvest.invoices.all
|
88
|
+
invoices.count.should == 1
|
89
|
+
|
90
|
+
invoices = harvest.invoices.all(:status => 'draft')
|
91
|
+
invoices.count.should == 1
|
92
|
+
|
93
|
+
invoices = harvest.invoices.all(:status => 'closed')
|
94
|
+
invoices.count.should == 0
|
95
|
+
|
96
|
+
invoices = harvest.invoices.all(:status => 'draft', :page => 1)
|
97
|
+
invoices.count.should == 1
|
98
|
+
|
99
|
+
invoices = harvest.invoices.all(:timeframe => {:from => Date.today, :to => Date.today})
|
100
|
+
invoices.count.should == 1
|
101
|
+
|
102
|
+
invoices = harvest.invoices.all(:timeframe => {:from => '19690101', :to => '19690101'})
|
103
|
+
invoices.count.should == 0
|
104
|
+
|
105
|
+
invoices = harvest.invoices.all(:updated_since => Date.today)
|
106
|
+
invoices.count.should == 1
|
107
|
+
|
108
|
+
invoices = harvest.invoices.all(:updated_since => '21121231')
|
109
|
+
invoices.count.should == 0
|
110
|
+
|
63
111
|
harvest.invoices.delete(invoice)
|
64
112
|
harvest.invoices.all.select {|p| p.number == "1000"}.should == []
|
65
113
|
end
|
@@ -3,10 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe 'harvest projects' do
|
4
4
|
it 'allows adding, updating and removing projects' do
|
5
5
|
cassette('project1') do
|
6
|
-
client
|
7
|
-
"name" => "Joe's Steam Cleaning",
|
8
|
-
"details" => "Building API Widgets across the country"
|
9
|
-
)
|
6
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
10
7
|
|
11
8
|
project = harvest.projects.create(
|
12
9
|
"name" => "Test Project",
|
@@ -27,10 +24,7 @@ describe 'harvest projects' do
|
|
27
24
|
|
28
25
|
it 'allows activating and deactivating clients' do
|
29
26
|
cassette('project2') do
|
30
|
-
client
|
31
|
-
"name" => "Joe's Steam Cleaning",
|
32
|
-
"details" => "Building API Widgets across the country"
|
33
|
-
)
|
27
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
34
28
|
|
35
29
|
project = harvest.projects.create(
|
36
30
|
"name" => "Test Project",
|
@@ -22,10 +22,7 @@ describe 'harvest tasks' do
|
|
22
22
|
context "task assignments" do
|
23
23
|
it "allows adding, updating, and removing tasks from projects" do
|
24
24
|
cassette('tasks2') do
|
25
|
-
client
|
26
|
-
"name" => "Joe's Steam Cleaning",
|
27
|
-
"details" => "Building API Widgets across the country"
|
28
|
-
)
|
25
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
29
26
|
|
30
27
|
project = harvest.projects.create(
|
31
28
|
"name" => "Test Project2",
|
@@ -66,10 +63,7 @@ describe 'harvest tasks' do
|
|
66
63
|
|
67
64
|
it "allows creating and assigning the task at the same time" do
|
68
65
|
cassette('tasks3') do
|
69
|
-
client
|
70
|
-
"name" => "Joe's Steam Cleaning 2",
|
71
|
-
"details" => "Building API Widgets across the country"
|
72
|
-
)
|
66
|
+
client = harvest.clients.create(FactoryGirl.attributes_for(:client))
|
73
67
|
|
74
68
|
project = harvest.projects.create(
|
75
69
|
"name" => "Test Project3",
|
@@ -7,12 +7,12 @@ describe Harvest::Expense do
|
|
7
7
|
it "should parse strings" do
|
8
8
|
date = RUBY_VERSION =~ /1.8/ ? "12/01/2009" : "01/12/2009"
|
9
9
|
expense = Harvest::Expense.new(:spent_at => date)
|
10
|
-
expense.spent_at.should ==
|
10
|
+
expense.spent_at.should == Date.parse(date)
|
11
11
|
end
|
12
12
|
|
13
13
|
it "should accept times" do
|
14
14
|
expense = Harvest::Expense.new(:spent_at => Time.utc(2009, 12, 1))
|
15
|
-
expense.spent_at.should ==
|
15
|
+
expense.spent_at.should == Date.new(2009, 12, 1)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -6,42 +6,41 @@ describe Harvest::Invoice do
|
|
6
6
|
invoice = Harvest::Invoice.new(:line_items => "kind,description,quantity,unit_price,amount,taxed,taxed2,project_id\nService,Abc,200,12.00,2400.0,false,false,100\nService,def,1.00,20.00,20.0,false,false,101\n")
|
7
7
|
invoice.line_items.count.should == 2
|
8
8
|
line_item = invoice.line_items.first
|
9
|
-
|
9
|
+
|
10
10
|
line_item.kind.should == "Service"
|
11
11
|
line_item.project_id.should == "100"
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
it 'parses csv into objects w/o projects' do
|
15
15
|
invoice = Harvest::Invoice.new(:line_items => "kind,description,quantity,unit_price,amount,taxed,taxed2,project_id\nService,Abc,200,12.00,2400.0,false,false,\nService,def,1.00,20.00,20.0,false,false,\n")
|
16
16
|
invoice.line_items.count.should == 2
|
17
17
|
line_item = invoice.line_items.first
|
18
|
-
|
18
|
+
|
19
19
|
line_item.kind.should == "Service"
|
20
20
|
line_item.description.should == "Abc"
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
it 'parses empty strings' do
|
24
24
|
invoice = Harvest::Invoice.new(:line_items => "")
|
25
25
|
invoice.line_items.should == []
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
it 'accepts rich objects' do
|
29
29
|
Harvest::Invoice.new(:line_items => [Harvest::LineItem.new(:kind => "Service")]).line_items.count.should == 1
|
30
30
|
Harvest::Invoice.new(:line_items => Harvest::LineItem.new(:kind => "Service")).line_items.count.should == 1
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
it 'accepts nil' do
|
34
34
|
Harvest::Invoice.new(:line_items => nil).line_items.should == []
|
35
35
|
end
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
context "as_json" do
|
39
39
|
it 'encodes line items csv' do
|
40
40
|
invoice = Harvest::Invoice.new(:line_items => "kind,description,quantity,unit_price,amount,taxed,taxed2,project_id\nService,Abc,200,12.00,2400.0,false,false,\nService,def,1.00,20.00,20.0,false,false,\n")
|
41
41
|
invoice.line_items.count.should == 2
|
42
42
|
invoice.line_items.first.kind.should == "Service"
|
43
|
-
|
44
|
-
invoice.as_json["doc"]["csv_line_items"].should == "kind,description,quantity,unit_price,amount,taxed,taxed2,project_id\nService,Abc,200,12.00,2400.0,false,false,\nService,def,1.00,20.00,20.0,false,false,\n"
|
43
|
+
invoice.as_json["invoice"]["csv_line_items"].should == "kind,description,quantity,unit_price,amount,taxed,taxed2,project_id\nService,Abc,200,12.00,2400.0,false,false,\nService,def,1.00,20.00,20.0,false,false,\n"
|
45
44
|
end
|
46
45
|
end
|
47
|
-
end
|
46
|
+
end
|