GLS100 – Journal Import Part III

Well, 3 posts in one day!  Breaking all the rules here (some good advice that I read a while ago was someone that prepared several posts and would drip feed them out so their blog would have at least one entry a month rather than fits and starts).

But advice is something that you give, not take – and I am waiting on desktop computers to finish patching!

So, GLS100 – Journal Import.  This is the third posting of this (https://potatoit.wordpress.com/2011/02/12/journal-importing-from-excel-jscript-handling-of-error-messages-part-ii/).

Recently I was made aware that there is an issue with this script under LSO10, it appears that this was caused by my code search through the VisualTree looking for the control that displayed any error messages – this control has been renamed – actually, it sounds like it is a different type of control now but we have the same effect.

For the regular readers, you’ll know that I discovered another method of retrieving the error information (https://potatoit.wordpress.com/2012/01/16/extracting-the-error-message/), so I have taken the opportunity to rip out all the superflous code – reducing the footprint quite considerably and providing what I believe a more robust method of error detection (which will work even if the user has the errors displayed in a dialog box)

I also took the opportunity to address an issue that a reader commented on about internationalisation – there is an issue where if you run an English version of Excel on a version of Windows that isn’t set to English, and you don’t have the appropriate language packs installed you get this pretty vague error.  Helpfully Microsoft had the code that I needed to fix the issue and it was pretty trivial to translate in to Javascript.

Then there was also a user reported issue which has since been addressed.

Anyways, to see the layout of the spreadsheet, please refer to my earlier posts.  Oh, it’s worth noting that I don’t have LSO10 installed, so I am trusting that the code as it stands now will work.  If you are on LSO10 and are still having an issue, feel free to drop me a comment with the details and I’ll see what I can do.

Enjoy…

// V004 20110207    * completed
// V005 20110207    * maximum row is now set to 10000
//                  * we will look for the word End to end our line processing
// V006 20110212    * don't submit lines that have a 0 value
//                  * truncate the decimals to two decimals
// V007 20120512    * updated error checking to something a tad more sensible
//                      so it will work with LSO 10
//      20120513    * addressed an issue with "Old format or invalid type library"
//                      http://support.microsoft.com/kb/320369
//                      issue raised by Hendrik
//                      https://potatoit.wordpress.com/2011/01/23/journal-importing-jscript-handling-of-error-messages/#comment-45
// V008 20120516    * if a forumla is used in the debit column and it is zero, 
//                      then we incorrectly don't process the credit column
// V009 20120516    * we stopped on blank lines when we should continue processing


import System;
import System.Text;
import System.Windows;
import System.Windows.Controls;
import System.Windows.Media;
import System.Windows.Media.Media3D;

import MForms;
import Mango.UI.Core;
import Mango.UI.Core.Util;
import Mango.UI.Services;
import Mango.Services;

import Excel;
import System.Reflection;

package MForms.JScript
{
	class GLS100_JournalImport_V09
	{
		var giicInstanceController : IInstanceController = null;    // this is where we will store the IInstanceController to make it available to the rest of the class
		var ggrdContentGrid : Grid = null;                          // this is the Grid that we get passed by the Init()
		var gexaApplication = null;                                 // Excel.Application object
		
		var gbtnImportFromExcel : Button = null;                    // this is the button that we will put on to the panel that will kick off the whole import
		var glvListView : ListView = null;                          // this is the ListView on the panel
		
		var gwbWorkbook = null;                                     // here we will store the Workbook object
		
		var giStartRow : int = 15;                                  // the starting row in the Spreadsheet
		var giMaxRow : int = 10000;                                    // the end row in the Spreadsheet
		var giCurrentRow : int = 15;                                // the current row in the Spreadsheet
		
		var gbLookForResponse = false;                              // should we be looking for a response?

		var gobjStatusJ1 = null;                                      // the statusbar
		var gobjStatusE = null;

		var gbRequest : boolean = false;                            // the request event 


		var gstrVoucherType : String = null;                        // the voucher type

		public function Init(element: Object, args: Object, controller : Object, debug : Object)
		{
			// lets make some of the controls and other
			// bits pieces available to other sections of our code
			ggrdContentGrid = controller.RenderEngine.Content;
			giicInstanceController = controller;
			glvListView = controller.RenderEngine.ListControl.ListView;
			
			try
			{
				// create the button for importing
				gbtnImportFromExcel = new Button();
				gbtnImportFromExcel.Content = "Import";

				Grid.SetColumnSpan(gbtnImportFromExcel, 10);
				Grid.SetColumn(gbtnImportFromExcel, 1);
				Grid.SetRow(gbtnImportFromExcel, 22);
				
				// finally add the control to the grid
				ggrdContentGrid.Children.Add(gbtnImportFromExcel);
				
				// ----- Events -----
				gbtnImportFromExcel.add_Click(OnImportFromExcelClicked);
				gbtnImportFromExcel.add_Unloaded(OnImportFromExcelUnloaded);

			}
			catch(exException)
			{
				MessageBox.Show("Error: " + exException.Message + Environment.NewLine + exException.StackTrace);
			}
		
		}
		
		// check for errors, we need to check
		// for errors on BOTH the E panel
		// and J1 panel
		// We will go out and look for the status control if we 
		// don't have it
		private function checkForError()
		{
			var strResult : String = null;
            // var objRuntime = MForms.Runtime.Runtime(giicInstanceController.Runtime).Result;
			var strStatusMessage : String = MForms.Runtime.Runtime(giicInstanceController.Runtime).Result;  // objRuntime.Result;

			try
			{
				var iStartPosition : int = 0;

				iStartPosition = strStatusMessage.IndexOf("<Msg>");

				// if Msg doesn't exist, then we didn't have an error
				if(-1 == iStartPosition)
				{
					// we are all good!
				}
				else
				{
					var iEndPosition : int = strStatusMessage.IndexOf("</Msg>");
					if((-1 == iEndPosition) && (0 != iEndPosition))
					{
						iEndPosition = strStatusMessage.length-1;
					}
					strResult = strStatusMessage.substring(iStartPosition+5, iEndPosition);
				}
			}
			catch(ex)
			{
				MessageBox.Show("checkForError() exception: " + ex.message);
			}

			return(strResult);
		}

		// display an OpenFileDialog box
		// and extract the result
		private function retrieveImportFile()
		{
			var result : String = null;
			var ofdFile = new System.Windows.Forms.OpenFileDialog();    // we have to use the forms OpenFileDialog unfortunately
			if(null != ofdFile)
			{
				ofdFile.Multiselect = false;
				ofdFile.Filter = "Excel Files (*.xls;*.xlsx)|*.xls;*.xlsx|All Files (*.*)|*.*"; // filter on xls or xlsx files only
				
				if(true == ofdFile.ShowDialog())
				{
					result = ofdFile.FileName;
				}
			}
			return(result);
		}
		
		// this is where we actually do the import
		private function OnImportFromExcelClicked(sender : Object, e : RoutedEventArgs)
		{
			gstrVoucherType = null;
			try
			{
				// here we do some initialisation of Excel
				InitialiseExcel();

				var strFilename : String = retrieveImportFile();            // retrieve the filename of the Excel spreadsheet to open
				if((null != strFilename) && (null != gexaApplication))      // ensure that not only do we have a filename, but we also managed to initialise Excel
				{
					gwbWorkbook = gexaApplication.Workbooks.Open(strFilename);  // open the spreadsheet
					if(null != gwbWorkbook)
					{
						giicInstanceController.add_RequestCompleted(OnRequestCompleted);
						giicInstanceController.add_RequestCompleted(OnRequested);
						gbRequest = true;

						gwbWorkbook.Saved = true;                               // get rid of those annoying save messages
						var strVoucherType : String = retrieveVoucherType();    // we want to get the voucher type from the spreadsheet (the GLS100 voucher)
						
						if(!String.IsNullOrEmpty(strVoucherType))               // we need to ensure that we have a voucher type
						{
							giicInstanceController.RenderEngine.SetFocusOnList();
							selectFAMFunction(strVoucherType);              // now we need to go out and select the function
							
							// from where on out, we start using the events
						}
					}
					else MessageBox.Show("Failed to Open Workbook");
				}
				else MessageBox.Show("Filename or Excel doesn't exist: " + strFilename);
			}
			catch(exException)
			{
				MessageBox.Show("Error: " + exException.description);
			}
		}

		// set the VoucherText within GLS100/E
		private function setM3VoucherText(astrVoucherText : String)
		{
			var tbVoucherText : TextBox = ScriptUtil.FindChild(ggrdContentGrid, "WWGVTX");
			if(null != tbVoucherText)
			{
				tbVoucherText.Text = astrVoucherText;
			}
			else MessageBox.Show("setM3VoucherText() - Child not found");
		}

		// set the Year within GLS100/E
		private function setM3YEA4(astrYEA4Text : String)
		{
			var tbYEA4Text : TextBox = ScriptUtil.FindChild(ggrdContentGrid, "WWYEA4");
			if(null != tbYEA4Text)
			{
				tbYEA4Text.Text = astrYEA4Text;
			}
			else MessageBox.Show("setM3VoucherText() - Child not found");
		}

		// we need to set the reversal date
		private function setM3ReversalDate(astrReversalText : String)
		{
			var tbReversalText = ScriptUtil.FindChild(ggrdContentGrid, "WWSHDT");
			if(null != tbReversalText)
			{
				try
				{
					var dtValue : DateTime = DateTime.FromOADate(Convert.ToDouble(astrReversalText));
					tbReversalText.Value = dtValue;
				}
				catch(ex)
				{
					MessageBox.Show(ex.description);
				}
			}
			else MessageBox.Show("setM3ReversalDate() - Child not found");
		}

		// set the accounting date within GLS100/E
		private function setM3AccountingDate(astrAccountingDate : String)
		{
			var tbITNO = ScriptUtil.FindChild(ggrdContentGrid, "WWACDT");
			if(null != tbITNO)
			{
				try
				{
					var dtValue : DateTime = DateTime.FromOADate(Convert.ToDouble(astrAccountingDate));
					tbITNO.Value = dtValue;
				}
				catch(ex)
				{
					MessageBox.Show(ex.description);
				}
			}
			else MessageBox.Show("Accounting Date not found");
		}

		// retrieve the voucher text from the spreadsheet
		private function retrieveVoucherText()
		{
			return(gwbWorkbook.ActiveSheet.Range("E7").Value);
		}
		
		// retrieve the accounting date from the spreadsheet
		// we need to use Value2 in this instance to get
		// a value that we can actually use
		private function retrieveAccountingDate()
		{
			try
			{
				return(gwbWorkbook.ActiveSheet.Range("R5").Value2);
			}
			catch(ex)
			{
				MessageBox.Show("Exception: " + ex.message);
			}
			
		}

		// retrieve the voucher type from the Spreadsheet
		private function retrieveVoucherType()
		{
			gstrVoucherType = gwbWorkbook.ActiveSheet.Range("K5").Value;
			return(gstrVoucherType);
		}		

		// retrieve the reversing date
		private function retrieveReversingDate()
		{
			try
			{
				return(gwbWorkbook.ActiveSheet.Range("R7").Value2);
			}
			catch(ex)
			{
			}
		}		


		// GLS100/B set the FAM Function
		private function selectFAMFunction(astrFAMFunction : String)
		{
			var bFound : boolean = false;
			
			if(!String.IsNullOrEmpty(astrFAMFunction))
			{
				// search through the ListView for the FAM function
				for(var iCount : int = 0; iCount < glvListView.Items.Count; iCount++)
				{
					var itmCurrentItem = glvListView.Items[iCount];
					if(null != itmCurrentItem)
					{
						if(!String.IsNullOrEmpty(itmCurrentItem[0]))
						{
							var strCurrentString = itmCurrentItem[0].ToString();
							if(0 == String.Compare(strCurrentString, astrFAMFunction))
							{
								glvListView.SelectedItem = itmCurrentItem;
								bFound = true;
								break;
							}
						}
					}
				}
			}
			if(true == bFound)
			{
				// ok, we've found the FAM Function on the ListView
				// now we need to SELECT it
				giicInstanceController.ListOption("1");	// SELECT
			}
			
		}
		
		// our Import button is being unloaded, now's a good time to clean
		// everything up
		private function OnImportFromExcelUnloaded(sender : Object, e : RoutedEventArgs)
		{
			if(null != gbtnImportFromExcel)
			{
				gbtnImportFromExcel.remove_Click(OnImportFromExcelClicked);
				gbtnImportFromExcel.remove_Unloaded(OnImportFromExcelUnloaded);
			}
		}
		
		public function OnRequested(sender: Object, e: RequestEventArgs)
		{
			// we don't really use this at all at the moment
		}

		// set the error line against the spreadsheet
		private function setLineStatus(astrError : String)
		{
			if(null != gwbWorkbook)
			{
				gwbWorkbook.ActiveSheet.Range("S" + (giCurrentRow-1).ToString()).Value = astrError;
			}
		}
	
		public function OnRequestCompleted(sender: Object, e: RequestEventArgs)
		{
			var strError : String = null;
			try
			{
				if(e.CommandType == MNEProtocol.CommandTypeKey)     // we're looking for a key event
				{
					if(e.CommandValue == MNEProtocol.KeyEnter)      // specifically we're looking the enter key event
					{
						strError = checkForError();

						if(true == String.IsNullOrEmpty(strError))
						{
							if(true == giicInstanceController.RenderEngine.PanelHeader.EndsWith("E"))   // are we on panel E?
							{
								handleEPanel();     // handle panel E
								strError = checkForError();
							}
							else if(true == giicInstanceController.RenderEngine.PanelHeader.EndsWith("J1")) // are we on panel G1 (this should be GLS120/G1)
							{
								strError = checkForError();

								if(true != String.IsNullOrEmpty(strError))
								{
									giCurrentRow = giMaxRow + 1;
								}
								else
								{
									handleJ1Panel();    // handle panel j
								}
							}
						}
						else
						{
							setLineStatus(strError);
						}
					}
				}
				else if(e.CommandType == MNEProtocol.CommandTypeListOption)
				{
					if(e.CommandValue == MNEProtocol.OptionSelect)
					{
						if(true == String.IsNullOrEmpty(strError))
						{
							handleEPanel();
						}
					}
				}
				if(null != giicInstanceController.Response)
				{
					if(0 == String.Compare(giicInstanceController.Response.Request.RequestType.ToString(), "Panel"))
					{
						if((MNEProtocol.CommandTypeKey == giicInstanceController.Response.Request.CommandType) && (MNEProtocol.KeyF03 == giicInstanceController.Response.Request.CommandValue))
						{
							CleanUp();
						}
					}
				}
			}
			catch(ex)
			{
				MessageBox.Show(ex.message);
			}
			if(null != strError)
			{
				CleanUp();
			}
		}
		
		// this is where we do the actual handling of the J1 Panel
		private function handleJ1Panel()
		{
			if(giCurrentRow <= giMaxRow)    // the spreadsheet has a limited number of rows...
			{
				// extract the lines from the spreadsheet
				var strWWADIV : String = retrieveFromActiveSheet("H" + giCurrentRow);       // division
				var strWXAIT1 : String = retrieveFromActiveSheet("B" + giCurrentRow);
				var strWXAIT2 : String = retrieveFromActiveSheet("C" + giCurrentRow);
				var strWXAIT3 : String = retrieveFromActiveSheet("D" + giCurrentRow);
				var strWXAIT4 : String = retrieveFromActiveSheet("E" + giCurrentRow);
				var strWXAIT5 : String = retrieveFromActiveSheet("F" + giCurrentRow);
				var strWXAIT6 : String = retrieveFromActiveSheet("G" + giCurrentRow);
				var strWWCUAMDebit : String = retrieveFromActiveSheet("I" + giCurrentRow);
				var strWWCUAMCredit : String = retrieveFromActiveSheet("L" + giCurrentRow);
				var strWWVTXT : String = retrieveFromActiveSheet("O" + giCurrentRow);
				var strWWVTCD : String = retrieveFromActiveSheet("N" + giCurrentRow);

				// this is the current row
				giCurrentRow = giCurrentRow + 1;
				if(!String.IsNullOrEmpty(strWXAIT1))
				{
					if(0 != String.Compare(strWXAIT1,"undefined"))  // verify that we actually have content
					{
						if(0 != String.Compare(strWXAIT1,"End", true))
						{
							var bDoWeHaveAValue : boolean = false;
							
							if(!String.IsNullOrEmpty(strWWCUAMDebit))
							{
								var dblDebit : double = strWWCUAMDebit;
								var strDebit : String = dblDebit.ToString("#.##");  // make sure that we are formatted to only 2 decimal places
								if(!String.IsNullOrEmpty(strDebit))                 // ensure we actually have a value now that we have converted it
								{
									if(0 != String.Compare(strDebit, "0.00"))       // ensure that the value isn't 0!
									{
										bDoWeHaveAValue = true;
										setM3TextField("WWCUAM", strDebit);	// Value
									}
								}
							}
							if((!String.IsNullOrEmpty(strWWCUAMCredit)) && (false == bDoWeHaveAValue))  // 20120516 - if a forumla is used in the debit column and it is zero, then we incorrectly don't process the credit column
							{
								var dblCredit : double = strWWCUAMCredit;
								var strCredit : String = dblCredit.ToString("#.##");    // make sure that we are formatted to only 2 decimal places
								if(!String.IsNullOrEmpty(strCredit))                    // ensure we actually have a value now that we have converted it
								{
									if(0 != String.Compare(strCredit, "0.00"))          // ensure that the value isn't 0!
									{
										bDoWeHaveAValue = true;
										setM3TextField("WWCUAM", "-" + strCredit);	// Value
									}
								}
							}
							if(true == bDoWeHaveAValue)     // if the value is 0, then we shouldn't submit it
							{
								// strWWADIV
								if(!String.IsNullOrEmpty(strWWADIV))
								{
									setM3TextField("WWADIV", strWWADIV);	// division
								}

								setM3TextField("WXAIT1", strWXAIT1);	// account
	
								setM3TextField("WXAIT2", strWXAIT2);	// Dept
								setM3TextField("WXAIT3", strWXAIT3);	// Dim3
								setM3TextField("WXAIT4", strWXAIT4);	// Dim4
								setM3TextField("WXAIT5", strWXAIT5);	// Dim5
								setM3TextField("WXAIT6", strWXAIT6);	// Dim6
								//setM3TextField("", retrieveFromActiveSheet("H" + giCurrentRow));	// Division

						
								setM3TextField("WWVTXT", strWWVTXT);	// Voucher Text
								setM3TextField("WWVTCD", strWWVTCD);    // VAT type

								giicInstanceController.PressKey("ENTER");   // press the enter key
							}
							else
							{
								// we need to go to the next line to process
								handleJ1Panel();
							}
						}
						else
						{
							giCurrentRow = giMaxRow + 1;    // 20110207 - end this loop
							CleanUp();                      // and do our cleanup
						}

					}
                    else handleJ1Panel();   //MessageBox.Show("2. No content in column B");
				}
                else handleJ1Panel();   //MessageBox.Show("1. No content in column B");
			}
			else
			{
				//MessageBox.Show("handleJ1Panel(): " + giCurrentRow + " - " + giMaxRow);
				CleanUp();
			}
		}
		
		// a little wee helper function that will search for a TextBox name
		// and set the TextBox value
		private function setM3TextField(astrName : String, astrValue : String)
		{
			var tbTextBox : TextBox = ScriptUtil.FindChild(ggrdContentGrid, astrName);
			if(null != tbTextBox)
			{
				tbTextBox.Text = astrValue;
			}
			else MessageBox.Show("Can't find: " + tbTextBox.Text);
		}

		// retrieve some data from the active spreadsheet
		// at a specific location
		private function retrieveFromActiveSheet(astrPosition : String)
		{
			var strValue : String = gwbWorkbook.ActiveSheet.Range(astrPosition).Value;
			if(true == String.IsNullOrEmpty(strValue))
			{
				strValue = "";
			}
			else if(0 == String.Compare(strValue, "undefined"))
			{
				strValue = "";
			}

			return(strValue);
		}
		
		// retrieveVoucherType
		// gstrVoucherType
		// handle the E Panel
		private function handleEPanel()
		{
			var strAccountingDate : String = retrieveAccountingDate();  // retrieve the accounting date from the Spreadsheet
			var strVoucherText : String = retrieveVoucherText();        // retroeve the voucher text from the Spreadsheet

			var strReversalDate : String = retrieveReversingDate();     // retrieve the reversal date
			var bHaveAllTheFields : Boolean = false;                    // do we have all the fields that we require?



			if( (0 == String.Compare(gstrVoucherType,"100")) || (0 == String.Compare(gstrVoucherType,"200")) || (0 == String.Compare(gstrVoucherType,"900")))
			{
				if((!String.IsNullOrEmpty(strAccountingDate)) && (!String.IsNullOrEmpty(strVoucherText)))
				{
					if(0 == String.Compare(gstrVoucherType,"900"))
					{
						setM3YEA4(strAccountingDate);
					}
					else
					{
						setM3AccountingDate(strAccountingDate);     // now we actually set the accounting date in the TextBox
					}
					
					setM3VoucherText(strVoucherText);           // and the Voucher TextBox
					bHaveAllTheFields = true;
				}
			}
			else if(0 == String.Compare(gstrVoucherType,"300"))
			{
				if((!String.IsNullOrEmpty(strReversalDate)) && (!String.IsNullOrEmpty(strAccountingDate)) && (!String.IsNullOrEmpty(strVoucherText)))
				{
					setM3AccountingDate(strAccountingDate);     // now we actually set the accounting date in the TextBox
					setM3VoucherText(strVoucherText);           // and the Voucher TextBox
					setM3ReversalDate(strReversalDate);         // now set the reversal date

					bHaveAllTheFields = true;
				}
			}
			else
			{
				MessageBox.Show("Sorry, but we can't handle the voucher type: " + gstrVoucherType);
			}
			// if((!String.IsNullOrEmpty(strAccountingDate)) && (!String.IsNullOrEmpty(strVoucherText)))
			if(true == bHaveAllTheFields)
			{
				giicInstanceController.PressKey("ENTER");   // now we press enter - this will fire off a Request event and should take us to GLS120/G1
			}
			else MessageBox.Show("We require an Account Date and Voucher Text");
		}
		
		private function CleanUp()
		{
			if(true == gbRequest)
			{
				giicInstanceController.remove_RequestCompleted(OnRequestCompleted);
				giicInstanceController.remove_RequestCompleted(OnRequested);
			}
			gbRequest = false;
			CleanUpExcel();
			//MessageBox.Show("Cleaned up");
		}

		private function CleanUpExcel()
		{
				// check to ensure we have a Workbook object
				// before we attempt to close the workbook
				if(null != gwbWorkbook)
				{
					gwbWorkbook.Close();
					gwbWorkbook = null;
				}
				// make sure we have actually created
				// the Excel Application object before
				// we Quit
				if(null != gexaApplication)
				{
					gexaApplication.Quit();
					gexaApplication = null;
				}
		}

		private function InitialiseExcel()
		{
			var result = null;
			try
			{
				gexaApplication = new ActiveXObject("Excel.Application");
				gexaApplication.Visible = true;
				
                // Address a 'bug' "Old format or invalid type library" where
                // you run an english version of Excel but the regional settings of the
                // computer is configured for a non-English language (and the language pack
                // isn't installed)
                //  http://support.microsoft.com/kb/320369
                // 
                System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");

				result = gexaApplication;
			}
			catch(exException)
			{
				MessageBox.Show("Error: " + exException.Message + Environment.NewLine + exException.StackTrace);
			}
			return(result);
		}
		
		//
	}
}

 

This entry was posted in How Far is Too Far?, M3 / MoveX. Bookmark the permalink.

2 Responses to GLS100 – Journal Import Part III

  1. Alexandra Ramos says:

    Hi,
    Great blog!!
    I’m starting to use the jscripts in LSO…I’m not a developer so this all new to me. I tried to use your code to import an Excel file to M3 but in Portugal all companies use Debit Credit sign so the code is incomplete. I’ve added a column in excel file with D or C letter and added the following line in the code: var strWWDBCR : String = retrieveFromActiveSheet(“J” + giCurrentRow), but I still have the debit/credit error message and the field DBCR is not populated. Can you help me?
    Thanks a lot!
    Kind Regards,
    Alexandra

    • potatoit says:

      Hi Alexandra,

      thank you for the comments.

      I’m not 100% sure I follow – feel free to flick me an email to my gmail.com account potatoit.blog@ with an example of the spreadsheet you are using and I’ll see if I can point you in the right direction.

      Cheers,
      Scott

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s