File Coverage

blib/lib/Business/Inventory/Valuation.pm
Criterion Covered Total %
statement 96 97 98.9
branch 33 36 91.6
condition 6 9 66.6
subroutine 9 9 100.0
pod 6 6 100.0
total 150 157 95.5


line stmt bran cond sub pod time code
1             package Business::Inventory::Valuation;
2              
3             our $DATE = '2018-03-17'; # DATE
4             our $VERSION = '0.007'; # VERSION
5              
6 1     1   57972 use 5.010001;
  1         9  
7 1     1   4 use strict;
  1         2  
  1         28  
8 1     1   5 use warnings;
  1         1  
  1         724  
9              
10             sub new {
11 10     10 1 13043 my $class = shift;
12 10         30 my %args = @_;
13              
14 10         19 my $self = bless {}, $class;
15              
16 10 100       52 die "Please specify method" unless $args{method};
17             die "Invalid method, please choose LIFO/FIFO/weighted average"
18 9 100       59 unless $args{method} =~ /\A(LIFO|FIFO|weighted average)\z/;
19 8         22 $self->{method} = delete $args{method};
20              
21 8         18 $self->{allow_negative_inventory} = delete $args{allow_negative_inventory};
22              
23 8 100       28 keys(%args) and die "Unknown argument(s): ".join(", ", keys %args);
24              
25 7         41 $self->{_inventory} = [];
26 7         16 $self->{_units} = 0;
27 7         11 $self->{_average_purchase_price} = undef;
28              
29 7         20 $self;
30             }
31              
32             sub buy {
33 17     17 1 589 my ($self, $units, $unit_price) = @_;
34              
35             # sanity checks
36 17 100       58 die "Units must be > 0" unless $units > 0;
37 15 50       32 die "Unit price must be >= 0" unless $unit_price >= 0;
38              
39 15 100 66     26 if (!@{ $self->{_inventory} }) {
  15 100       41  
    100          
40 7         11 push @{ $self->{_inventory} }, [$units, $unit_price];
  7         19  
41 7         14 $self->{_units} = $units;
42 7         16 $self->{_average_purchase_price} = $unit_price;
43 8         54 } elsif (@{ $self->{_inventory} } && $self->{_inventory}[-1][1] == $unit_price) {
44 1         2 my $old_units = $self->{_units};
45 1         2 $self->{_inventory}[-1][0] += $units;
46 1         3 $self->{_units} += $units;
47             $self->{_average_purchase_price} = (
48             $old_units * $self->{_average_purchase_price} +
49 1         4 $units * $unit_price) / $self->{_units};
50             } elsif ($self->{method} eq 'weighted average') {
51 2         4 my $old_units = $self->{_units};
52 2         5 $self->{_inventory}[0][0] = $self->{_units} = $self->{_units} + $units;
53             $self->{_inventory}[0][1] = $self->{_average_purchase_price} = (
54             $old_units * $self->{_average_purchase_price} +
55 2         7 $units * $unit_price) / $self->{_units};
56             } else {
57 5         12 push @{ $self->{_inventory} }, [$units, $unit_price];
  5         15  
58 5         15 my $old_units = $self->{_units};
59 5         11 $self->{_units}+= $units;
60             $self->{_average_purchase_price} = (
61             $old_units * $self->{_average_purchase_price} +
62 5         23 $units * $unit_price) / $self->{_units};
63             }
64             }
65              
66             sub sell {
67 18     18 1 442 my ($self, $units, $unit_price) = @_;
68              
69             # sanity checks
70 18 100       51 die "Units must be > 0" unless $units > 0;
71 16 50       37 die "Unit price must be >= 0" unless $unit_price >= 0;
72              
73 16         22 my $profit = 0;
74 16         21 my $units_sold = 0;
75              
76 16 100       49 if ($self->{_units} < $units) {
77 7 100       15 if ($self->{allow_negative_inventory}) {
78 4         6 $units = $self->{_units};
79             } else {
80 3         32 die "Attempted to oversell ($units, while inventory only has ".
81             "$self->{_units})";
82             }
83             }
84              
85 13         22 my $remaining = $units;
86 13         21 my $orig_average_purchase_price = $self->{_average_purchase_price};
87              
88             # due to rounding error, _units and _inventory might disagree for a bit
89 13   66     34 while ($self->{_units} > 0 && @{ $self->{_inventory} } && $remaining > 0) {
  17   66     70  
90 17         27 my $item;
91 17 100       39 if ($self->{method} eq 'LIFO') {
92 9         18 $item = $self->{_inventory}[-1];
93             } else {
94 8         12 $item = $self->{_inventory}[0];
95             }
96              
97 17 100       35 if ($item->[0] > $remaining) {
98             # inventory item is not used up
99 6         11 my $old_units = $self->{_units};
100 6         12 $item->[0] -= $remaining;
101 6         12 $self->{_units} -= $remaining;
102 6 50       13 if ($self->{_units} == 0) {
103 0         0 undef $self->{_average_purchase_price};
104             } else {
105             $self->{_average_purchase_price} = (
106             $old_units * $self->{_average_purchase_price} -
107 6         20 $remaining * $item->[1]) / $self->{_units};
108             }
109 6         13 $profit += $remaining * ($unit_price - $item->[1]);
110 6         12 $units_sold += $remaining;
111 6         10 $remaining = 0;
112 6         70 goto RETURN;
113             } else {
114             # inventory item is used up, remove from inventory
115 11 100       21 if ($self->{method} eq 'LIFO') {
116 7         11 pop @{ $self->{_inventory} };
  7         14  
117             } else {
118 4         7 shift @{ $self->{_inventory} };
  4         4  
119             }
120 11         20 $units_sold += $item->[0];
121 11         17 $remaining -= $item->[0];
122 11         17 my $old_units = $self->{_units};
123 11         14 $self->{_units} -= $item->[0];
124 11         18 $profit += $item->[0] * ($unit_price - $item->[1]);
125 11 100       20 if ($self->{_units} == 0) {
126 6         19 undef $self->{_average_purchase_price};
127             } else {
128             $self->{_average_purchase_price} = (
129             $old_units * $self->{_average_purchase_price} -
130 5         20 $item->[0] * $item->[1]) / $self->{_units};
131             }
132             }
133             }
134              
135             RETURN:
136 13         21 my @return;
137 13 100       24 if (defined $orig_average_purchase_price) {
138 12         30 push @return, $units *
139             ($unit_price - $orig_average_purchase_price);
140             } else {
141 1         2 push @return, undef;
142             }
143 13         19 push @return, $profit;
144 13         21 push @return, $units_sold;
145 13         72 @return;
146             }
147              
148             sub inventory {
149 26     26 1 64 my $self = shift;
150 26         42 @{ $self->{_inventory} };
  26         188  
151             }
152              
153             sub units {
154 24     24 1 57 my $self = shift;
155 24         98 $self->{_units};
156             }
157              
158             sub average_purchase_price {
159 24     24 1 52 my $self = shift;
160 24         99 $self->{_average_purchase_price};
161             }
162              
163              
164             1;
165             # ABSTRACT: Calculate inventory value/unit price (using LIFO/FIFO/weighted average)
166              
167             __END__