Adding Fields to a Panel and the Customer Extension Table

I’m waiting for a database export when it occurred to me that I haven’t discussed the Customer Extension table which is really terrible as I’ve been using it for quite a while and it is awesome!

The customer extension table APIs allow us to store data within M3 that M3 doesn’t have fields with. There are some fantastic examples in the net change reports where it is combined with the native M3 events to populate data based on different events automatically, and of-course we can use them through the APIs to store additional data. We can also incorporate these values in to the new style ListViews without any code!

The APIs provided are quick and easy to use.

This post is to provide a Jscript which allows you to add a TextBox to a panel and it will store the data from the TextBox in the customer extension table – how often do people want to do that? 🙂

Figure 1 – example of incorporating data from the customer extension table without coding around the ListView

Figure 2 – comboboxes that store additional data for the csutomer in the customer extension table (this has a Jscript to provide the fields)

The script I’m posting today is designed around being generic so if you want to add a single field, it’s pretty easy to use just by specifying some arguments to the script. And it provides a starting point to customising to your needs.

The script will do some checks to make sure we have a minimum number of arguments before adding a Label and a TextBox to the panel and will also look at an existing TextBox on the panel to determine if we should allow the user to edit our TextBox or if we should only display our value.

It will check to see if there is a matching value in the customer extension table and if there is populate our TextBox.

We will then wait for a Next event, if this occurs we will retrieve the values from our Key TextBoxes and our custom TextBox and write the data to the customer extension table.

Pretty simple really!

Arguments are as follows:

,,,,,,,,[PK02],[PK03],[PK04],[PK05],[PK06],[PK07],[PK08]

The yellow highlighted arguments relate to the UI, the green highlighted relate back to the database.

Label Text    the Text we display in the label for our TextBox
Label Column    the grid column placement for the label
Label Width    the width of the label (we will automatically calculate the column span)
Column    the column for the TextBox
Width    the width of the TextBox
Row    the Grid Row we will set for the Label and TextBox
Read Only Key Field    this is the TextBox that we will check to see if we are in display or edit mode
CUG File    the F1FILE name we will use – this is one of the search keys so should be unique and meaningful
PK01    the name of a TextBox we will use as part of our key for lookup, stored in F1PK01

Optional fields

[PK02..PK08]    up to 7 additional TextBoxes we can use as keys – typically these should not be changeable by the user and will be used for our record lookup. They are stored in F1PK02 – F1PK08

So what does it look like?

Using the script with the following arguments:
My Label,40,100,51,100,1,MMITNE,SACTEST,WEITNO

Yields this.

Figure 3 – Script loaded when the panel is in edit mode

We use the Ext item Number TextBox to determine if we are in display, so when it is read only we set our TextBox to read only (technically we just change the style)

Figure 4 – script loaded when we are in display mode

Now if we take a look at the database…SACTEST was what we assigned the CUG File (F1FILE) so we were search on that

F1FILE = SACTEST
F1PK01 = our item number
F1A030 = our stored value

We can also see the F1PK01 – F1PK08 fields, these are indexed and the script allows all 8 to be used as our ‘uniqueness’ values.

And now the script itself.


/*
**  Name:   AddControl.js
**
**  Description:
**   Adds a textbox to a panel the contents of which will be read from/saved to the customer extension tables
**	 Uniqiness is defined by the PKxx keys which must be associated with textboxes
**	 We also handle situations where we are in display mode by keying off an existing textbox which changes from
**	 read only to editable depending on display/edit
**
**	Notes:
**		we get the keys from the UI at the point we save, this means that if the key textboxes are changable, we may get
**		not have the expected results, however this may be the expected result 🙂
**
**		If the user enters a blank value in to our textbox we don't delete the record, we just save the A030 as blank
**
**		Don't use comments in the label text!
**
**  Arguments:
**
**  <label text>,<label column>,<label width>,<column>,<width>,<row>,<read only key field>,<CUG File>,<PK01>,[PK02],[PK03],[PK04],[PK05],[PK06],[PK07],[PK08]
**
**  Eg on MMS001/E
**      My Label,40,100,51,100,1,MMITNE,SACTEST,WEITNO
**			- My Label	= the label we will put next to the textbox
**			- 40 = the grid column the label goes
**			- 100 = with width of the label
**			- 51 = the grid column of the textbox
**			- 100 = width of the textbox
**			- 1 = the row we will use for the label and textbox
**			- MMITNE = the TextBox that will change from being read only and editable depending on whether we display/edit
**			- SACTEST = the F1FILE field that we will store against in the Customer Extension table (CUGEX1)
**			- WEITNO = this will be the first and only of our keys
**			In the CUGEX1 table we will have
**				F1FILE = SACTEST
**				F1PK01 = <item number>
**				F1A030 = <the value of our textbox>
**
**  Written By: scott.campbell@potatoit.kiwi
**
**
*/
 
import System;
import System.Windows;
import System.Windows.Controls;
import MForms;
 
import Mango.UI.Core;
import Lawson.M3.MI;
 
package MForms.JScript
{
    class AddControl
    {
        var gDebug = null;
		var gController = null;
		var gContent = null;
		
		var gTextBox : TextBox = new TextBox();
		
		// this is the control that we will use to determine if
		// we should be read only
		var gReadOnlyControl = null;
		var gReadOnlyControlName = null;
		
		var gControlColumn = -1;
		var gControlRow = -1;
		var gControlWidth = -1;
		var gLabelColumn = -1;
		var gLabelWidth = -1;
		var gLabelContent = null;
		
		var gCUGFile = null;
		
		// store our TextBoxes here
		var gKeyField1 = null;
		var gKeyField2 = null;
		var gKeyField3 = null;
		var gKeyField4 = null;
		var gKeyField5 = null;
		var gKeyField6 = null;
		var gKeyField7 = null;
		var gKeyField8 = null;
		
		// have we finished retrieving our value?
		var gRetrieveValue = false;
 
        public function Init(element: Object, args: Object, controller : Object, debug : Object)
        {
			gController = controller;
            gContent = controller.RenderEngine.Content;
            gDebug = debug;
            try
            {
                if(null != args)
                {
                    var argumentArray = args.split(",");
 
                    if(null != argumentArray)
                    {
                        if(argumentArray.length > 8)
                        {
							for(var i = 0; i < argumentArray.length; i++)
							{
								var currentArgument = argumentArray[i];
								if(false == String.IsNullOrWhiteSpace(currentArgument))
								{
									switch(i)
									{
										case 0:
											gLabelContent = currentArgument;
											break;
										case 1:
											gLabelColumn = currentArgument;
											break;
										case 2:
											gLabelWidth = currentArgument;
											break;
										case 3:
											gControlColumn = currentArgument;
											break;
										case 4:
											gControlWidth = currentArgument;
											break;
										case 5:
											gControlRow = currentArgument;
											break;
										case 6:
											gReadOnlyControlName = currentArgument;
											break;
										case 7:
											gCUGFile = currentArgument;
											break;
										case 8:
											gKeyField1 = getTextBox(currentArgument);
											break;
										case 9:
											gKeyField2 = getTextBox(currentArgument);
											break;
										case 10:
											gKeyField3 = getTextBox(currentArgument);
											break;
										case 11:
											gKeyField4 = getTextBox(currentArgument);
											break;
										case 12:
											gKeyField5 = getTextBox(currentArgument);
											break;
										case 13:
											gKeyField6 = getTextBox(currentArgument);
											break;
										case 14:
											gKeyField7 = getTextBox(currentArgument);
											break;
										case 15:
											gKeyField8 = getTextBox(currentArgument);
											break;
									}
								}
							}
							
							if(true == doWeHaveEnoughParameters())
							{
								gReadOnlyControl = ScriptUtil.FindChild(gContent, gReadOnlyControlName);
								
								if(null != gReadOnlyControl)
								{
									// we have passed our preliminary checks, add the controls to the panel
									addControls();
									// subscribe to the events so we can capture the next event
									subscribeToEvents();
									
									// if we are in display, we should set our control to read only
									if(true == areWeReadOnly(gReadOnlyControl))
									{
										gTextBox.Style = gReadOnlyControl.Style;
									}
									
									// attempt to retrieve the value we will populate the TextBox with 
									// from the customer extension table
									var request = CUSEXTMI_GetFieldValue(gCUGFile, safeGetTextBoxValue(gKeyField1), safeGetTextBoxValue(gKeyField2), safeGetTextBoxValue(gKeyField3), safeGetTextBoxValue(gKeyField4), safeGetTextBoxValue(gKeyField5), safeGetTextBoxValue(gKeyField6), safeGetTextBoxValue(gKeyField7), safeGetTextBoxValue(gKeyField8));
									MIWorker.Run(request, OnGetFieldValueRunComplete);
								}
								else
								{
									gDebug.Error("Failed to locate the read only control (" + gReadOnlyControlName + "), exiting");
								}
							}
							else
							{
								gDebug.Error("We don't have enough parameters");
							}
                        }
						else
						{
							gDebug.Error("Not enough parameters");
						}
                    }
 
                }
            }
            catch(ex)
            {
                debug.Error(ex);
            }
        }
		
		private function getTextBox(aName)
		{
			var result = null;
			
			if(false == String.IsNullOrWhiteSpace(aName))
			{
				result = ScriptUtil.FindChild(gContent, aName);
				
				if(null == result)
				{
					gDebug.Error("Failed to locate TextBox: " + aName);
				}
			}
			
			return(result);
		}
		
		// a safe retrieval of the TextBox Text field
		private function safeGetTextBoxValue(aTextBox)
		{
			var result = null;
			
			if(null != aTextBox)
			{
				result = aTextBox.Text;
			}
			
			return(result);
		}

		// check to see if we have enough valid values to continue
		private function doWeHaveEnoughParameters()
		{
			var result = false;
			if(
				null != gLabelContent &&
				-1 != gLabelColumn &&
				-1 != gLabelWidth &&
				-1 != gControlColumn &&
				-1 != gControlWidth &&
				-1 != gControlRow &&
				false == String.IsNullOrWhiteSpace(gReadOnlyControlName) &&
				false == String.IsNullOrWhiteSpace(gCUGFile) &&
				false == String.IsNullOrWhiteSpace(gKeyField1)
			)
			{
				result = true;
			}
			return(result);
		}
		
		public function OnGetFieldValueRunComplete(response : MIResponse)
		{
			if(null != response)
			{
				if(false == response.HasError)
				{
					if(null != gTextBox && response.Item != null)
					{
						gTextBox.Text = response.Item.GetString("A030");
						gRetrieveValue = true;
					}
				}
			}
		}
 
		public function OnRequested(sender : Object, e : RequestEventArgs)
		{
			if(MNEProtocol.CommandTypeKey == e.CommandType && MNEProtocol.KeyEnter == e.CommandValue)
			{
				var request : MIRequest = null;
				if(true == gRetrieveValue)
				{
					request = CUSEXTMI_ChgFieldValue(gCUGFile, safeGetTextBoxValue(gKeyField1), safeGetTextBoxValue(gKeyField2), safeGetTextBoxValue(gKeyField3), safeGetTextBoxValue(gKeyField4), safeGetTextBoxValue(gKeyField5), safeGetTextBoxValue(gKeyField6), safeGetTextBoxValue(gKeyField7), safeGetTextBoxValue(gKeyField8), safeGetTextBoxValue(gTextBox));
					gDebug.Debug(request.Record.ToLogString());
				}
				else
				{
					request = CUSEXTMI_AddFieldValue(gCUGFile, safeGetTextBoxValue(gKeyField1), safeGetTextBoxValue(gKeyField2), safeGetTextBoxValue(gKeyField3), safeGetTextBoxValue(gKeyField4), safeGetTextBoxValue(gKeyField5), safeGetTextBoxValue(gKeyField6), safeGetTextBoxValue(gKeyField7), safeGetTextBoxValue(gKeyField8), safeGetTextBoxValue(gTextBox));
				}
				
				// fire and forget
				MIWorker.Run(request, OnSaveRunCompleted);
			}
			if(MNEProtocol.CommandTypePage != e.CommandType)
			{
				unsubscribeFromEvents();
			}
		}

		public function OnSaveRunCompleted(response : MIResponse)
		{
			//
		}
		
		private function subscribeToEvents()
		{
			gDebug.Debug("Subscribing to events");
			gController.add_Requested(OnRequested);
		}
 
		private function unsubscribeFromEvents()
		{
			gDebug.Debug("Unsubscribing from events");
			gController.remove_Requested(OnRequested);
		}
 
		private function areWeReadOnly(aReadControl)
		{
			var result = false;
			if(null != aReadControl)
			{
				if(StyleManager.StyleTextBoxDisabled == aReadControl.Style)
				{
					result = true;
				}
			}
			return(result);
		}
 
		private function addControls()
		{
			var label = new Label();
			label.Content = gLabelContent;
			label.Width = gLabelWidth;
			setPosition(label, gLabelColumn, gControlRow);
			gContent.Children.Add(label);
			
			var gridColumnWidth = calculateGridColumnWidth(gContent);
			
			var labeltextBoxColumnSpan = 0;
			
			var textBoxColumnSpan = 0;
			
			if(0 != gridColumnWidth)
			{
				textBoxColumnSpan = gControlWidth / gridColumnWidth;
				labeltextBoxColumnSpan = gLabelWidth / gridColumnWidth;
				
				if(0 === gControlWidth % gridColumnWidth)
				{
					textBoxColumnSpan += 1;
				}
				if(0 === gLabelWidth % gridColumnWidth)
				{
					labeltextBoxColumnSpan += 1;
				}
				
				Grid.SetColumnSpan(gTextBox, textBoxColumnSpan);
				Grid.SetColumnSpan(label, labeltextBoxColumnSpan);
			}
			
			gTextBox.Height = Configuration.ControlHeight;
			gTextBox.Margin = Configuration.ControlMargin;
			gTextBox.Padding = Configuration.ControlPadding;
			gTextBox.Width = gControlWidth;
			gTextBox.MaxLength = 30;
			
			setPosition(gTextBox, gControlColumn, gControlRow);
			gContent.Children.Add(gTextBox);
		}
		
		private function calculateGridColumnWidth(aContent)
		{
			var result = 0;
			if(null != aContent && null != aContent.ColumnDefinitions && aContent.ColumnDefinitions.Count > 0)
			{
				result = aContent.ColumnDefinitions[0].ActualWidth;
			}
			return(result);
		}
 
        private function setPosition(aControl, aColumn, aRow)
        {
            if(null != aControl)
            {
                gDebug.Debug("Set Controls Position");
                gDebug.Debug(" +-- Control: " + aControl.Name);
                gDebug.Debug(" +---- Column: " + aColumn);
                gDebug.Debug(" +---- Row: " + aRow);
                Grid.SetColumn(aControl, aColumn);
                Grid.SetRow(aControl, aRow);
            }
        }
		
		private function CUSEXTMI_GetFieldValue(aFile, aPK01, aPK02, aPK03, aPK04, aPK05, aPK06, aPK07, aPK08)
		{
			var request = new MIRequest();
			var inRecord = new MIRecord();
			
			inRecord["FILE"] = aFile;
			inRecord["PK01"] = aPK01;
			
			if(null != aPK02)	inRecord["PK02"] = aPK02;
			if(null != aPK03)	inRecord["PK03"] = aPK03;
			if(null != aPK04)	inRecord["PK04"] = aPK04;
			if(null != aPK05)	inRecord["PK05"] = aPK05;
			if(null != aPK06)	inRecord["PK06"] = aPK06;
			if(null != aPK07)	inRecord["PK07"] = aPK07;
			if(null != aPK08)	inRecord["PK08"] = aPK08;			
			
			// if(false == String.IsNullOrWhiteSpace(aPK02))	inRecord["PK02"] = aPK02;
			// if(false == String.IsNullOrWhiteSpace(aPK03))	inRecord["PK03"] = aPK03;
			// if(false == String.IsNullOrWhiteSpace(aPK04))	inRecord["PK04"] = aPK04;
			// if(false == String.IsNullOrWhiteSpace(aPK05))	inRecord["PK05"] = aPK05;
			// if(false == String.IsNullOrWhiteSpace(aPK06))	inRecord["PK06"] = aPK06;
			// if(false == String.IsNullOrWhiteSpace(aPK07))	inRecord["PK07"] = aPK07;
			// if(false == String.IsNullOrWhiteSpace(aPK08))	inRecord["PK08"] = aPK08;
			
			request.Record = inRecord;
			
			var parameters = new MIParameters();
			var outFields : String[] = new String[1];
			outFields[0] = "A030";
			parameters.OutputFields = outFields;
			
			request.Program = "CUSEXTMI";
			request.Transaction = "GetFieldValue";
			
			return(request);
		}
		
		private function CUSEXTMI_Save(aTransaction, aFile, aPK01, aPK02, aPK03, aPK04, aPK05, aPK06, aPK07, aPK08, aA030)
		{
			var request = new MIRequest();
			var inRecord = new MIRecord();
			
			inRecord["FILE"] = aFile;
			inRecord["PK01"] = aPK01;
			
			if(null != aPK02 && false == String.IsNullOrWhiteSpace(aPK02))	inRecord["PK02"] = aPK02;
			if(null != aPK03 && false == String.IsNullOrWhiteSpace(aPK03))	inRecord["PK03"] = aPK03;
			if(null != aPK04 && false == String.IsNullOrWhiteSpace(aPK04))	inRecord["PK04"] = aPK04;
			if(null != aPK05 && false == String.IsNullOrWhiteSpace(aPK05))	inRecord["PK05"] = aPK05;
			if(null != aPK06 && false == String.IsNullOrWhiteSpace(aPK06))	inRecord["PK06"] = aPK06;
			if(null != aPK07 && false == String.IsNullOrWhiteSpace(aPK07))	inRecord["PK07"] = aPK07;
			if(null != aPK08 && false == String.IsNullOrWhiteSpace(aPK08))	inRecord["PK08"] = aPK08;
			// if(false == String.IsNullOrWhiteSpace(aPK02))	inRecord["PK02"] = aPK02;
			// if(false == String.IsNullOrWhiteSpace(aPK03))	inRecord["PK03"] = aPK03;
			// if(false == String.IsNullOrWhiteSpace(aPK04))	inRecord["PK04"] = aPK04;
			// if(false == String.IsNullOrWhiteSpace(aPK05))	inRecord["PK05"] = aPK05;
			// if(false == String.IsNullOrWhiteSpace(aPK06))	inRecord["PK06"] = aPK06;
			// if(false == String.IsNullOrWhiteSpace(aPK07))	inRecord["PK07"] = aPK07;
			// if(false == String.IsNullOrWhiteSpace(aPK08))	inRecord["PK08"] = aPK08;
			inRecord["A030"] = aA030;
			
			request.Record = inRecord;
			request.Program = "CUSEXTMI";
			request.Transaction = aTransaction;
			
			return(request);
		}
		
		private function CUSEXTMI_ChgFieldValue(aFile, aPK01, aPK02, aPK03, aPK04, aPK05, aPK06, aPK07, aPK08, aA030)
		{
			return(CUSEXTMI_Save("ChgFieldValue", aFile, aPK01, aPK02, aPK03, aPK04, aPK05, aPK06, aPK07, aPK08, aA030));
		}

		private function CUSEXTMI_AddFieldValue(aFile, aPK01, aPK02, aPK03, aPK04, aPK05, aPK06, aPK07, aPK08, aA030)
		{
			return(CUSEXTMI_Save("AddFieldValue", aFile, aPK01, aPK02, aPK03, aPK04, aPK05, aPK06, aPK07, aPK08, aA030));
		}
    }
}

Enjoy,
Scott

This entry was posted in Development, M3 / MoveX. Bookmark the permalink.

18 Responses to Adding Fields to a Panel and the Customer Extension Table

  1. Lode Vlaeminck says:

    In which scenario would you advice to use this solution vs a database extension?

    • potatoit says:

      A database extension? If you mean changing the M3 database tables / adding new tables to the M3 database, then I would council against doing that very strongly – it will come back to bite you.

      The customer extension tables have been provided for the purpose of storing additional data to prevent changes to the structure of the M3 database – though this script only takes advantage of a single field, there are multiple alpha fields, multiple numeric fields which are available in a single record / API call.

      If a record was storing very large numbers of fields for a record then you can split it in to multiple records or you could consider creating your own database outside of M3 and using MWS webservices to manipulate the record.

  2. Lode Vlaeminck says:

    I meant adding extra tables using MAK (M3 adaptation Kit). For instance creating ZITMAS that has the same index as MITMAS but holds additional item master data.

    • potatoit says:

      Infor created the extension tables to get away from people modifying M3 via MAK. I strongly recommend that people work to get rid of modifications and get to standard M3, it makes support and upgrades far quicker and easier. If people are considering modifications, I will do my best to convince them that this is not the route to go – I’ve seen far too many companies regret their modifications come upgrade or even patch time.

      Infor, like most ERP vendors is moving towards hosting and though Infor have a product where you can bring your mods in to the cloud, it will become harder and more expensive as they push everyone towards standard multi-tentanted solutions.

      Cheers,
      Scott

  3. Magnus Tallqvist says:

    Please note that in 13.4 there is a Metadata function added to the customer extension fields (CDF). The function is called CMS080 and through that name, description, type etc are maintained for each CDF. Please also note that from 13.4 the metadata entry is mandatory to be able to use the CDF´s

    • potatoit says:

      Thanks Magnus,

      I gather that this metadata is mandatory if you are using the native M3 functionality to access the data?
      Is there any implications from an upgrade perspective where customers are using the extended toolboxes to display data from the customer extension tables?

      And for peoples reference, you can read about CMS080 in KB: 1824822 and document ID 49129 entitled
      “M3 13.4 – Business Engine Net Change Overview – What s new in detail”

  4. Magnus Tallqvist says:

    Both the API (CUSEXTMI) and the usage in configurable lists requires a corresponding metadata record in CMS080 in order for it to be possible to use the CDF´s. It is a one time activity to add the records into CMS080 at an upgrade so it is not a bit task but you need to be aware. Secondly it gives a number of advantages like field descriptions, validation of values etc. Through the usage of the text (classic F6) in CMS080 you can also get into a sort of user defined help text through the script (text ID is included in the CMS080MI transaction).

  5. yvon says:

    Hi, good post.
    I have an error when launching the script

    DEBUG: Set Controls Position
    DEBUG: +– Control:
    DEBUG: +—- Column: 50
    DEBUG: +—- Row: 6
    ERROR: TypeError: Type mismatch

    an idea please
    Thank

    • potatoit says:

      Hi Yvon,

      check the script arguments, things like the label column, widths etc have to be numbers – if you’ve missed a field or a comma then you’ll probably get a similar message.

      The other thing that may do it is the “read only key field”, it must be a TextBox and it should be a TextBox that will toggle between enabled and disabled depending on whether the user is changing or displaying the data in the panel.

      Cheers,
      Scott

      • yvon says:

        Hi Scott,

        I dont understand, I have a copy paste of your arguments and on the MMS001 / E:

        My Label, 40,100,51,100.1, MMITNE, SACTEST, WEITNO

        I dont understand the second problem you. What should I do please

        thank you
        Cheers
        Yvon

      • potatoit says:

        In the arguments you’ve pasted, you have

        My Label, 40,100,51,100.1, MMITNE, SACTEST, WEITNO

        The arguments in my article are different
        My Label,40,100,51,100,1,MMITNE,SACTEST,WEITNO

        Note that you have
        100.1, MMITNE
        rather than
        100,1,MMITNE

        Cheers,
        Scott

      • yvon says:

        I’m sorry,

        In fact I have

        My Label,40,100,51,100,1,MMITNE,SACTEST,WEITNO

        the message is :

        DEBUG: Set Controls Position
        DEBUG: +– Control:
        DEBUG: +—- Column: 40
        DEBUG: +—- Row: 1
        ERROR: TypeError: Type mismatch

        I have that letter M that appears

        Help thank

      • potatoit says:

        Using those arguments on MMS001/E works without error in my environment.

        Are you using those arguments on MMS001/E?
        What version of Smart Office (including the build)?

        Cheers,
        Scott

  6. yvon says:

    Yes in MMS001/E

    Version : 10.2.1.0.200
    BE 15.1.4 MCP1 – 1514 – 05

    Cheers
    Yvon

    • potatoit says:

      Hi Yvon,

      please see the comments from Magnus on this article. He mentions that there needs to be a CMS080 entry for the field you’re adding.

      When I get some free time with a 13.4 environment, I’ll take a look at modifying this script so it can automatically generated the info or documenting what needs to be done.

      Cheers,
      Scott

      • yvon says:

        Hi Scott,

        thank you for your reply. I will look at the CMS080

        Cheers
        Yvon

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