Blog

RSS
Fixed Rate when less than X kg, otherwise per kg rate- Wednesday, May 16, 2012

The last few posts have been fairly complex scenarios.  I'll be posting some more simple shipping examples...

The shipping computation we'll configure is:

If the total weight is less than 5 kg, then the shipping will be a flat rate of $20.00, 

and if the total weight is 5 kg or more, then there will be a shipping rate of $4.00 per kilo.



Note the Name Expression on the above option.  This is optional.  In this case, it allows you to show the actual weight in the Shipping Rate Name shown to the customer.  E.g.

    Shipping for 9.1 kg ($36.40)

You can also choose to use a Ceiling math function in the Rate Expression so that fractions of weight roll up to the next whole integer:

   Math.Ceiling([$TotalWeight]) * 4.00

Shipping for 9.1 kg ($40.00)

Comments (0)
More Complex Free Shipping Scenario- Monday, April 23, 2012

This is a more complex free shipping scenario that involves multiple free shipping methods (Fedex Ground & Standard), and also a "$5 Next Day Shipping".  So, some items can get Free Ground, other items can get Free Next Day, and other items can get $5 Next Day.  I won't go into too much detail about how this one works, so you should read the prior blog to understand the idea of packing for free shipping, and the multiple Option type records.  I know it looks a little daunting.  In the future I hope to provide a simpler interface, or the nopCommerce core will support product level shipping configuration.

Similar to last blog, separate Categories are used to pidgeonhole the products.  Note the Reference variable hasReducedNextDayShipping;  this covers both free next day and $5 next day.   The Fedex Name for standard next day is "FedEx Standard Overnight".   The idea is to first use Option(FedEx) to get the rates for all the products, then Pack/OptionReplace to get (replace) the rates for non-reduced items.  Finally, add in rate for $5 Next day items.

Note the new Packing method "Packing.ClearPackages".  This restores the original "single package" which is the default state prior to any other packing methods being used.  This is necessary because the Option record type with a  Rate Expression that is not a shipping method like Shipping.Fedex applies the Rate Expression for each package; we just want one package because we calculate variable "Number of $5 NextDay Items" based on sum of item quantities.  Then the last Option type record adds the appropriate number of $5 per item charges to the "FedEx Standard Overnight" rate (after it's been previously calculated by Fedex for the non-reduced next day items if any).

EvalType

Name

Expression

RateExpression

NameExpression

DescriptionExpression

Reference

HasCategory

ProductVariant.Product.ProductCategories.Any

 

 

 

Reference

hasReducedGroundShipping

[@HasCategory]
(Category.Name.Contains("Ground Shipping"))

 

 

 

Reference

hasReducedNextDayShipping

[@HasCategory]
(Category.Name.Contains("Next Day Shipping"))

 

 

 

Reference

has5$NextDayShipping

[@HasCategory]
(Category.Name = "$5 Next Day Shipping")

 

 

 

Integer

Number of $5 NextDay Items

Items.Where
([@has5$NextDayShipping]).Sum(Quantity)

 

 

 

Option

FedEx All Items

true

Shipping.Fedex

 

 

Packing

Pack exclude reduced-ground items

Items.Any([@hasReducedGroundShipping])

Packing.FirstFitSingleBox

 

[@hasReducedGroundShipping]

OptionReplace

FedEx Ground Only

Items.Any([@hasReducedGroundShipping])

Shipping.Fedex

[$Name].Contains("Ground")
 ? [$Name] : ""

 

Packing

Pack exclude
reduced-nextday items

Items.Any([@hasReducedNextDayShipping])

Packing.FirstFitSingleBox


[@hasReducedNextDayShipping]

OptionReplace

FedEx NextDay Only

Items.Any([@hasReducedNextDayShipping])

Shipping.Fedex

[$Name].Contains("Standard")
 ? [$Name] : ""

 

Packing

Clear Packages For Fixed Rates

true

Packing.ClearPackages

 

 

Option

$5 NextDay

Items.Any([@has5$NextDayShipping])

[Number of $5 NextDay Items] * 5.00

"FedEx Standard Overnight"

 

Tags :  FreeShipping
Comments (2)
Packing removes Free Shipping items- Monday, April 23, 2012

When configuring a product variant you can select "Free Shipping:".   However, there are a few caveats you should know.

1) If ALL the items in the cart are marked Free Shipping, then the shipping charge will be $0 regardless of any shipping method or configuration (country/weight, etc).  Even Shipping Director cannot override this, as it is calculated in the core system.

2) Free Shipping is currently not respected by online rate methods (FedEx, UPS, USPS, etc.).  It is respected by the offline methods ByTotal, and ByWeight.  And, it's also respected by Shipping Director's Packing.

By "respected", I mean that all the product items will be counted in the weight/dimensions.  To get an accurate rate, you need to only get a rate based on the weight/dimensions of the non-free-shipping items.

So, a simple setup to use the product variant Free Shipping with an online method looks like this

Type

Name

Expression

Rate Expression

Packing

Pack ignores items marked FreeShipping

true

Packing.FirstFitSingleBox

OptionExit

FedEx only sees packed items

true

Shipping.Fedex

However, the above will (for items that are not free shipping) calculate rates  for ALL the shipping methods - Ground, Priority, Standard, etc.

If you want to only offer Free Ground Shipping then it is necessary to first get a rate quote for all items, and then get a rate quote for just the non-free-shipping items for just the Ground method.  But, as pointed out above, if ALL items are marked Free Shipping in the product variant, then we always get a $0 rate.  So, we can't use the Free Shipping checkbox on the variant.  Instead, I recommend using a Category.  Set up a "Free Ground Shipping" Category, and mark it as unpublished (i.e. uncheck "Published" check box).  If the category is unpublished, you can still put products in the category, but it won't appear in the category navigation tree or elsewhere.   (If you need free ground on only a specific variant of a product, then you'll have to use some other mechanism; we won't cover that here.)

To get Ground only rates, we need to exclude certain methods (e.g. Priority, Standard).  For that we need to use the Option record type's ability to modify the shipping method Name returned by the offline shipping method; if the Name Expression evaluates to blank (""), then the entire method/rate is excluded.    Also, we need to replace the the first Ground rate calculated on all the product items.  For that, we use the new OptionReplace type.  The "Option" type adds rates to existing same-Name methods, whereas "OptionReplace" will replace the rate.

So, assuming we are not using product variant Free Shipping and instead using Categories, it looks like this - Get rates for all methods, pack only the non-Free Shipping items (by excluding Free-Shipping items), get the rates again but only keep the Ground rate replacing the original Ground rate.

EvalType

Name

Expression

Rate Expression

Name Expression

Description Expression
(Exclude)

Reference

hasFreeGroundShipping

Product.HasCategory("Free Ground Shipping")

 

 

 

Option

FedEx All Items

true

Shipping.Fedex

 

 

Packing

Pack exclude free-ground items

Items.Any([@hasFreeGroundShipping])

Packing.FirstFitSingleBox

 

[@hasFreeGroundShipping]

OptionReplace

FedEx Ground Only

Items.Any([@hasFreeGroundShipping])

Shipping.Fedex

[$Name].Contains("Ground")
? [$Name] : ""

 

Don't forget, you can use the $Debug variable as your first record, and this will provide detailed processing messages in the System Log (view source of page for readability).

EvalType

Name

Expression

Boolean

$Debug

true


(The above blog was updated 9/8/2013.  Older versions of Shipping Director did not have HasCategory() function, and used a more complex expression to get the Category – it had two References rather than one:

EvalType

Name

Expression

Reference

HasCategory

ProductVariant.Product.ProductCategories.Any

Reference

hasFreeGroundShipping

[@HasCategory](Category.Name =
 "Free Ground Shipping")


Tags :  PackingFreeShipping
Comments (0)
Shipping Director 1.05 available for NopCommerce 2.5- Monday, April 23, 2012

We've released Shipping Director 1.05 for NopCommerce 2.50.  You can download it from the Download Trial link on its product page.

I'll blogged about the new features shortly.  In brief:
  Support for complex Free Shipping and Reduced Shipping scenarios  (by Product, Category, etc.)

  Support for multi-location shipping
    - Pass location info to other shipping plugins
    - Sum rates for same method name :  e.g. FedEx Ground & UPS Ground  ==> "Ground"

  Can pack multiple times for complex scenarios

  Fabricate a package of given dimensions (CreatePackageFromBoxDims)
    - e.g. Convert Quantity into a Package Weight so you can use ByWeight shipping plugin's method/country/weight setup

  HTML entry in expression fields (for example, you can put map links in shipping description field)

  XML parsing - e.g. allows access to Checkout Attributes  ( SelectSingleNode() ) 

  Goto linenumber  (for complex flow control)

Comments (0)
Packing by Warehouse (Sender Expression with Ship From Information)- Friday, March 16, 2012

It’s valuable to pack items in separate boxes to calculate rates per box (package), and using the Package record type’s Sender Expression is one way to do it.  The Sender Expression calculates a Sender Identifier on a package, and the real power of using Sender Expression when packing is to override the default sending location (ZipCode & Country).    NopCommerce has defined Ship From fields in the shipping rate request, and Shipping Director supports them.   In Shipping Director, the Ship From fields can be set up when packing by using the Sender Expression; when the special tags “ZipCode=” and “Country=” are seen in the string result of the Sender Expression, then when passing packages to other shipping plugins, the tag values are used to set the Ship From fields.  The tags are of the form “tag=value;” – the value is everything between the “=” and the terminating “;”.  For example, “ZipCode=10021;Country=US;”.

By default, when a Sender identifier is seen on the first package, then the Shipping Rate Request type is “One Request per Sender” - i.e. $ShippingRateRequest=OneRequestPerSender.  This means that all the packages with the same sender identifier are sent in a single request to the other shipping plugin.  If there are multiple sender identifiers, then multiple requests are made. Then, the rates for the same shipping option name (e.g. “FedEx Ground”) are summed together.

The above Sender Expression “ZipCode=10021;Country=US;” illustrates that the expression is of String type, but isn’t a very useful example.   In the real world, you need some way of calculating the Ship From location based on some data in the system.  In the below example, two “warehouse” Categories have been configured with Published not checked so that the Categories do not appear to the customer.  The Names of the categories have in them the Ship From tags as described above:

Warehouse 1:ZipCode=48126;Country=US;

Warehouse 2: ZipCode=11735;Country=US;

Then, Products have been assigned to the Categories.

Here’s what the Shipping Director records look like – we use Reference types to reference the product’s categories that have “ZipCode” in the name.  The ‘sender’ reference will evaluate using a condition (the ternary  “ ? : “ operator) - If a product is not in a “warehouse” category, then the expression evaluates to “Main”, and since “Main” does not have the tags in it, the plugin’s own configured ship from information will be used.  - If a product is in a “warehouse” category, then the expression evaluates to the Name of the Category which has the Ship From tags in it.


Order

Type

Name

Expression

Rate Expression

Name Expression

100

Reference

warehouseCategories

ProductVariant.Product.ProductCategories .FirstOrDefault(Category.Name.Contains("ZipCode="))

 

 

110

Reference

sender

[@warehouseCategories] = null ? "Main" : [@warehouseCategories].Category.Name

 

 

120

Packing

Pack By Warehouse

true

Packing.FirstFitSingleBox

[@sender]

130

OptionExit

FedEx

true

Shipping.Fedex

 

Final Note: Not all of the external shipping rate methods built into nopCommerce allow for dynamically changing the Ship From address; some shipping plugins only use the fields specified on their configuration screens.  The only shipping plugins that currently support overriding the shipping location are UPS, USPS, and the modified FedEx available on this site.  [Update 2012-Sept: nopC 2.50 Fedex added this capability]

Tags :  WarehousePacking
Comments (1)
Shipping Director 1.03 available for NopCommerce 2.4- Monday, February 20, 2012

We've released Shipping Director 1.03 for NopCommerce 2.4.  You can download it from the Download Trial link on its product page.

We've corrected a localization problem, and added additional localization capability.  This is most useful when you want to display numeric values – many languages use different decimal and thousands separators – e.g. “,” vs. “.”.   For example, in English, [$TotalWeight].ToString() would show “6.25”, but in Spanish it would show “6,25”.

Also, you can now set up error messages, etc. for all your languages, and use the new GetLocaleString() function to get a language specific message.

This is a "special function" - it must be assigned to a String variable and must be the first word in the variable’s Expression field.  The expression cannot be complex:  it can be like GetLocaleString("..."), or GetLocaleString([someotherstringvar]), but cannot include any operators.  

As an example, you should validate that Country State Zip have been entered because the front end does not enforce it on Estimate Shipping.  Create a language resource string for each of your languages using the Resource Name like Shipping.Director.PleaseEnterCountryStateZip, and assign appropriate Values for each language.

 Order

Type

Name

Expression

Description Expression

10

String

PleaseEnterCountryStateZip

GetLocaleString("Shipping.Director.PleaseEnterCountryStateZip")

 

20

ErrorExit

Missing Country State Zip

ShippingAddress.Country = null or ShippingAddress.ZipPostalCode = null or ("US,CA".Contains(ShippingAddress.Country.TwoLetterIsoCode) and ShippingAddress.StateProvince = null)

[PleaseEnterCountryStateZip]

Also, in case there are any unforeseen problems during calculation you may want to localize the default “Sorry, we are unable to calculate your shipping.  Please contact the store."   That's already there as Resource Name:  Plugins.Shipping.Director.Error.UnableToCalculateShipping

Similarly, we’ve added a GetSetting() special function.  It can be assigned to an appropriate variable type based on the setting’s type.

We’ve also added additional debugging capability.  The $Debug variable was there previously, but was mostly for packing and rate tracing.  Now it will also show variable assignments, etc.  Just add a Boolean record with name $Debug.  By default it is false.

 Order

Type

Name

Expression

Description Expression

0

Boolean

$Debug

true

 

Check the system log for the messages.  Previously, “debugging” was best accomplished by using Error type records, so now you don't have to do extra Error records for that purpose.  Because the log view page is html, the messages will all appear concatenated.  To read it more clearly, right click the page and view source.  Scroll down a bit and you'll see each message on its own line.

And finally, we’ve moved the trial check to the end of the evaluation routine.  This way, the records will always be evaluated, and messages, etc. will be logged; you will still see the trial warning on the web page after 5 transactions, but it will still evaluate and log the messages. This will allow you to continue testing without having to reset the app or reload the plugins.

Comments (0)
SQL Example - Lookup table for Zip to Zone, and lookup table for Zone/Weight to Rate- Sunday, February 12, 2012

If you have a complex rate lookup, or if you just prefer to write your rate calculation as a SQL Server stored procedure, Shipping Director has the ability to do SQL queries and call stored procedures. 

If an expression starts with one of these keywords, then the expression is evaluated as a SQL query:

            SELECT

            ?SELECT"

            EXECUTE"

            ?EXECUTE

Note that there are two forms of SELECT and EXECUTE; one form is prefixed with a “?”.  Without the leading “?” the query is treated as a standard SQL paramaterized query using “@”-prefixed parameters.  For example:

select Zone from ShippingZone_Origin23235 where @p1 between PrefixFrom and PrefixTo; [ZipCode3]

 This default behavior should be used when there is a concern for SQL injection.  (It is also recommended to use ErrorExit to test for for such possibilities.)

With the leading “?” the query can contain zero-based indexed placeholders (format items to be used in String.Format).  For example:

?select Zone from ShippingZone_Origin{0} where '{1}' between PrefixFrom and PrefixTo;[OriginZipCode],[ZipCode3]

This provides the ability to create truly dynamic queries.  In the above example, the table name contains a format specifier.

The result of a SQL expression needs to be a single scalar value (i.e. a String, a Decimal, etc.).  Typically the expression is assigned to a variable.

An example of using SQL queries would be to do a custom rate lookup from a set of Zone/Weight/Rate  tables in the database.  The first table would be used to lookup a Zone base on a shipping destination zip code, and the second table would use the looked up Zone and the total weight of items in the cart to lookup the rate.


ShippingZone_Origin23235

PrefixFrom

PrefixTo

Zone

120

126

4

127

127

3

128

147

4

148

163

3

164

165

4

166

172

3

ShippingZone_Rate

Weight

Zone2

Zone3

Zone4

Zone5

Zone6

Zone7

Zone8

Zone9

1

5.17

5.4

5.51

5.75

6.04

6.12

6.22

19.18

2

5.37

5.72

6.22

6.34

6.75

6.89

7.13

21.3

3

5.45

5.97

6.53

6.73

7.15

7.36

7.88

23.21

4

5.58

6.16

6.86

7.17

7.53

7.85

8.44

25.37

5

5.79

6.25

7.15

7.46

7.83

8.2

8.91

27.63

6

5.96

6.44

7.27

7.65

7.97

8.44

9.1

30.01


The first option is to use parameterized queries (“select” without the “?” prefix).  In this example, a constant Origin Zip Code is provided, but this could instead be a calculation of a specific warehouse based on what is in the cart.

SQL queries using parameterized queries

 

Order

Type

Name

Expression

 Rate

 Expression

200

String

ZipCode3

ShippingAddress.ZipPostalCode.Substring(0,3)

 

300

String

Zone

select Zone from ShippingZone_Origin23235 where @p1 between PrefixFrom and PrefixTo; [ZipCode3]

 

350

Decimal

ZoneRate

select Rate from ShippingZone_Rate where Zone = @p1 and Weight = Ceiling(@p2); [Zone],[$TotalWeight]

 

500

OptionExit

Shipping Cost

true

 [ZoneRate]


 

 

 

 

Use a stored procedure to do the lookup  (replace lines 300 and 350 above with just this line 300)

300

 

Decimal

ZoneRate

EXECUTE ShippingZone_LookupRate @p1, @p2; [ZipCode3], [$TotalWeight]

 

The second option is to use the “?” prefix so that the table name can be dynamically determined.  In this example, a constant Origin Zip Code is provided, but this could instead be a caclulation of a specific warehous based on what is in the cart. 

SQL queries using zero-based index placeholders

Order

Type

Name

Expression

 Rate

 Expression

100

String

OriginZipCode

"23235"

 

200

String

ZipCode3

ShippingAddress.ZipPostalCode.Substring(0,3)

 

300

String

Zone

?select Zone from ShippingZone_Origin{0} where '{1}' between PrefixFrom and PrefixTo;[OriginZipCode],[ZipCode3]

 

350

Decimal

ZoneRate

?select Zone{0} from ShippingZone_Rate where Weight = Ceiling({1}); [Zone],[$TotalWeight]

 

500

OptionExit

Shipping Cost

true

 [ZoneRate]

 

 

 

 

 

Use a stored procedure to do the lookup  (replace lines 300 and 350 above with just this line 300)

300

 

Decimal

ZoneRate

?EXECUTE ShippingZone_LookupRate '{0}',{1}; [ZipCode3], [$TotalWeight]


Here's an example stored procedure.  Note that the SELECT statements only return a single column (and should only return a single row)

CREATE PROCEDURE [dbo].[ShippingZone_LookupRate]
(
    @zip     VARCHAR(9),
    @weight  DECIMAL(8,4)
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @zone VARCHAR(3);
    IF @WEIGHT = 0 
      SET @weight = 1;

    select @zone = Zone from ShippingZone_Origin23235 where SUBSTRING(@zip,1,3) between PrefixFrom and PrefixTo

    IF      @zone = '2' select Zone2 from ShippingZone_Rate where Weight = Ceiling(@weight)
    ELSE IF @zone = '3' select Zone3 from ShippingZone_Rate where Weight = Ceiling(@weight)
    ELSE IF @zone = '4' select Zone4 from ShippingZone_Rate where Weight = Ceiling(@weight)
    ELSE IF @zone = '5' select Zone5 from ShippingZone_Rate where Weight = Ceiling(@weight)
    ELSE IF @zone = '6' select Zone6 from ShippingZone_Rate where Weight = Ceiling(@weight)
    ELSE IF @zone = '7' select Zone7 from ShippingZone_Rate where Weight = Ceiling(@weight)
    ELSE IF @zone = '8' select Zone8 from ShippingZone_Rate where Weight = Ceiling(@weight)
    ELSE IF @zone = '9' select Zone9 from ShippingZone_Rate where Weight = Ceiling(@weight)    
END

Tags :  SQLLookupTable
Comments (6)
Purchase Rules - Checking minimum quantities- Sunday, February 12, 2012

Besides calculating rates, Shipping Director can enforce all types of purchase rules.  For example, If you want to enforce that if items are purchased from a specific category that there be at least n of them, then you want an Error expression that looks like "if there are Any items in category A and the Quantity of those items is less than n":  

Order

Active

Type

Name

Expression

Rate Expression

Surcharge Expression

Name Expression

Description Expression

10

 

Reference

categories

ProductVariant.Product.ProductCategories

 

 

 

 

20

 

Reference

categoriesAnyA

[@categories].Any(Category.Name = "Category A")

 

 

 

 

30

 

ErrorExit

Not Enough Items from A

Items.Any([@categoriesAnyA]) and Items.Where([@categoriesAnyA]).Sum(Quantity) < 2

 

 

 

"Not Enough Items from Category A"

You would need to repeat lines 20 & 30 for each category  

(The customer won't get warning until they get to shipping calc page, or estimate shipping)

NOTE:  As of Shipping Director 1.08 for 2.80, you don't need line 10, and line 20's Reference should be: ProductVariant.Product.HasCategory("Category A")  

NOTE: For Shipping Director for 3.10, you don't need line 10, and line 20's Reference should be: Product.HasCategory("Category A")  

Tags :  MinimumQuanity
Comments (0)
Shipping Director 1.02 available for NopCommerce 2.4- Saturday, February 4, 2012

We've released Shipping Director 1.02 for both NopCommerce 2.3 and 2.4.
You can download it from the Download Trial link on its product page.

If you already have version 1.01 installed in the Plugins/Shipping.Director folder, then back it up, and then overwrite the files from what's in the downloaded .zip file.  You do not need a new License.txt file.  After you've uploaded/overwritten your files, you need to restart nopC, or reload plugins.  Either

  a) Login in as Administrator, goto Administration. Click “Restart application” (upper right), or

  b) Login in as Administrator, goto Administration > Configuration > Plugins > click the button "Reload list of Plugins"

In Configuration > Plugins,  scroll down to Shipping Director, and verify that it says version 1.02.

These are the new features:

1) Relaxed license check allows www.mydomain.com as well as mydomain.com.

2) Automatic Update capability.  If you have a prior version installed, it will detect it and upgrade settings, locale strings, etc.  To install, overwrite the files in the plugin folder with what's in the downloaded .zip file.

3) Packing - "Exclude Item Expression".  You can suppress items from being packed.  (E.g. can handle complex free shipping scenarios - unpacked items do not contribute to package  weight or dimensions)

4) $PackageCount variable.   Set after packing with the total # of packages packed.  If you are using packing, then you should check this after the Packing  type, so that you can indicate "Free Shipping" if all items got excluded.

5) $ShippingOptionsCount variable. Set after using an external shipping plugin with the total # of shipping rate options it returned.  Not typically used, but If you are doing something where you think no options will match, then you should check it and say something like "call for rate".

6) Localize Error Message "Sorry, we are unable to calculate your shipping. ..."  : Plugins.Shipping.Director.Error.UnableToCalculateShipping

Comments (0)
Example - Local Pickups and Local Delivery- Saturday, February 4, 2012

Maybe you're a small local store or restaurant, or pizza place; you want to offer local pickup at one of your two locations, or you will deliver nearby.  Here's an example using State, but could also check ShippingAddress.ZipPostalCode or ShippingAddress.City 

 

Order

Active

Type

Name

Expression

Rate Expression

Surcharge Expression

Name Expression

Description Expression

10

 

ErrorExit

No State Entered

ShippingAddress.StateProvince = null

 

 

 

"Please enter Country, State, and Zip"

20

 

ErrorExit

No Zip Entered

ShippingAddress.ZipPostalCode = null

 

 

 

"Please enter Country, State, and Zip"

50

 

String

ShippingState

ShippingAddress.StateProvince.Name

 

 

 

 

100

 

Option

In Store A Pickup

true

0

 

 

 

110

 

Option

In Store B Pickup

true

0

 

 

 

200

 

OptionExit

Local Delvery

[ShippingState] = "local 1"

10

 

 

 

210

 

OptionExit

Local Delivery

[ShippingState] = "local 2"

20

 

 

 

300

 

ErrorExit

Out of Area

 

 

 

 

"Sorry, we can’t deliver to that area"

Also, this example shows fixed Rates, but you can also calculate some percentage of the cost, or fixed rate plus percentage, etc. using

[$SubTotalWithoutDiscounts]

[$SubTotalWithDiscounts]

e.g.   Rate =  5 + ( [$SubTotalWithDiscounts] * 0.05 )

(Note that only US and Canada have StateProvince configured by default in nopCommerce, so adjust/remove the above ErrorExit if needed)


Tags :  LocalPickup
Comments (4)
 First ... Previous 5 6 7 8 9 Next