Solved

Using Scales with Devicehub without having Warehouse "Automated Warehouse Operations".

  • 30 April 2021
  • 6 replies
  • 581 views

Userlevel 2
Badge

Is there a way to connect a single scale, maybe two or three in time, to Devicehub without having to purchase "Automated Warehouse Operations"? It seems kind of silly that we can’t even connect one.

Will we ever purchase "Automated Warehouse Operations"? Likely in time but we have a long way to go before getting into the Manufacturing and Warehousing aspects of Acumatica and we’d like to be utilizing this in Shipping now.

Is there a compromise or work around anyone’s thought of that works?

 

Thanks ;-)

icon

Best answer by Gabriel Michaud 30 April 2021, 21:35

View original

6 replies

Userlevel 7
Badge +10

@jmckinnon I think DeviceHub is available regardless of your license; that’s all you need to integrate with the scale. The problem is that the standard Shipments screen doesn’t support the scale, but a developer can create a customization that pulls in the current weight from the user’s scale and inputs it in the current row in the Packages tab. Here’s a mockup of what I have in mind:

Do you work with a consultant that is familiar with the Acumatica framework? I don’t have a USB scale to test, but sample code to read the current scale weight can be found in PickPackShip.cs, in the ProcessScaleWeight function (extracted here in case anyone is interested) -- basically it just reads from the SMScale table to get the last received weight, and does some sanity checks in case the weight hasn’t changed in the last 30 seconds:

        protected virtual bool? ProcessScaleWeight(SOPackageDetailEx package)
{
Guid? scaleDeviceID = UserSetup.For(Base).ScaleDeviceID;

Base.Caches<SMScale>().ClearQueryCache();
SMScale scale = SMScale.PK.Find(Base, scaleDeviceID);

if (scale == null)
{
ReportError(Msg.ScaleMissing, "");
return false;
}

DateTime dbNow = GetServerTime();

if (scale.LastModifiedDateTime.Value.AddHours(1) < dbNow)
{
ReportError(Msg.ScaleDisconnected, scale.ScaleID);
return false;
}
else if (scale.LastWeight.GetValueOrDefault() == 0)
{
if (HeaderView.Current.LastWeighingTime == scale.LastModifiedDateTime.Value)
{
SkipBoxWeightInput();
return true;
}
else
{
ReportWarning(Msg.ScaleNoBox, scale.ScaleID);
Prompt(Msg.ScaleSkipPrompt);
HeaderSetter.Set(h => h.LastWeighingTime, scale.LastModifiedDateTime.Value);
return null;
}
}
else if (scale.LastModifiedDateTime.Value.AddSeconds(ScaleWeightValiditySeconds) < dbNow)
{
ReportError(Msg.ScaleTimeout, scale.ScaleID, ScaleWeightValiditySeconds);
return null;
}
else
{
decimal weight = ConvertKilogramToWeightUnit(scale.LastWeight.GetValueOrDefault(), CommonSetupUOM.Current.WeightUOM);
SetPackageWeight(weight);
return true;
}
}

 

Userlevel 2
Badge

 

@Gabriel Michaud

 

Regarding the Error…

I thought the error was on the client app itself, I’ll do some due diligence and follow-up with some details to be sure and I’ll be crossing my fingers that you’re right.

 

Regarding the customization…

This is C#, right? I haven’t gotten into that part yet. I see there’s a lot of checking / error trapping going on… guess that’s par for the course lol In addition to this then I suspect I’d also have to focus on the readings from the specific scale, just in case there’s two of them being used. on two different stations.

 

Thanks again for your guidance! maybe this will serve as my introduction into C#. ;-)

Userlevel 2
Badge

@jmckinnon @Gabriel Michaud - do you know if any company has deployed the customization idea that Gabriel proposed? This could be an extremely useful tool for us to start doing all of our packing and shipping within Acumatica directly. 

Userlevel 7
Badge +2

Hello thread posters,

Those interested in one-on-one discussion, please post here.  We would like to hear more about the challenges covered in this Community Post.

@MichaelTobi @jmckinnon 

 

-thanks, Dana

Is it possible to capture weights from a device-hub connected scale for inventory transactions, e.g. when issuing a raw material ingredient to a Production Order, or receipting a PO quantity?  Does this require a customisation too?

 

Userlevel 2

@fionabruce Yes, it requires a customization. But a DeviceHub connected scale should work for Production Orders or PO Receipts.

I needed a similar customization as described by @Gabriel Michaud. My company ships within Acumatica, but we don’t use Automated Warehouse Operations for pick/pack/ship. I was able to add a “Get Weight” action button to the Packages tab/grid of the Shipment Entry screen and have it use a Device Hub connected scale (e.g. Mettler Toledo PS60.) I’ve pasted the code below for anyone who wants a head-start.

If Acumatica would add support to DeviceHub for dimensioners... then I'd have something. We have 12 packaging lines and our box sizes are variable, so we are hand-keying W,L,H for every box. It's error prone.

Note: DeviceHub uses an API connection for scales, which appear to stay logged-in. I'm assuming this is for each connected scale. We have 15+ scales and several other connected applications which also rely on API connections (e.g. e-commerce, analytics, etc.)

I don't have all the scales online yet with DeviceHub, but I'm concerned I'm going to create a denial-of-service situation whereby the scales are constraining other higher priority API calls. Seems silly (to me) that Acumatica treats DeviceHub calls against your API limits.

I added a non-persisting boolean usrScaleRegistered field to the packages grid to allow me to (en|dis)able the action button based on if the scale is present for the logged-in user.

public class SOShipmentEntry_Extension : PXGraphExtension<SOShipmentEntry>
{
protected SMScale UserScale { get; set; }
public virtual int ScaleWeightValiditySeconds => 150;

public override void Initialize()
{
base.Initialize();

this.UserScale = GetScale();
}

protected virtual SMScale GetScale()
{
// get default scale value from user profile
var userPrefs = UserPreferences.PK.Find(Base, Base.Accessinfo.UserID);
Guid? scaleDeviceID = userPrefs?.DefaultScalesID;

Base.Caches<SMScale>().ClearQueryCache();

SMScale scale = SMScale.PK.Find(Base, scaleDeviceID);

return scale;
}

#region Actions & Buttons
public PXAction<SOShipment> getWeight;
[PXUIField(DisplayName = "Get Weight", MapViewRights = PXCacheRights.Select, MapEnableRights = PXCacheRights.Select)]
[PXButton(ImageKey = PX.Web.UI.Sprite.Main.DataEntryF, Tooltip = "Get Weight from Scale")]
protected virtual IEnumerable GetWeight(PXAdapter adapter)
{
SOPackageDetailEx current = Base.Packages.Current;

if (current != null)
{
// begin sanity checks
if (UserScale == null)
{
Base.Document.Ask("Scale", Msg.ScaleMissing, MessageButtons.OK);
}

DateTime now = GetServerTime();
if (UserScale.LastModifiedDateTime.Value.AddHours(1) < now)
{
Base.Document.Ask("Scale", Msg.ScaleDisconnected, MessageButtons.OK);
}

if (UserScale.LastModifiedDateTime.Value.AddSeconds(ScaleWeightValiditySeconds) < now)
{
Base.Document.Ask("Scale", Msg.ScaleTimeout, MessageButtons.OK);
}
else if (UserScale.LastWeight.GetValueOrDefault() != 0m)
{
decimal weight = Math.Round(ConvertKilogramToWeightUnit(UserScale.LastWeight.GetValueOrDefault(), CommonSetupUOM.Current.WeightUOM), 2);

Base.Packages.Cache.SetValueExt<SOPackageDetail.weight>(current, weight);
Base.Packages.Cache.SetValueExt<SOPackageDetail.confirmed>(current, true);
Base.Packages.Update(current);
}
}

return adapter.Get();
}
#endregion

#region Events
protected virtual void SOPackageDetailEx_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
SOPackageDetail row = e.Row as SOPackageDetail;
if (row == null) return;

SetScaleRegistered(sender, row);
}

protected virtual void SOPackageDetailEx_BoxID_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e)
{
SOPackageDetail row = e.Row as SOPackageDetail;
if (row == null) return;

SetScaleRegistered(sender, row);
}
#endregion

#region Utility
protected void SetScaleRegistered(PXCache sender, SOPackageDetail package)
{
// until proven otherwise
bool scaleRegistered = false;

if (UserScale != null && package.BoxID != null)
{
scaleRegistered = true;
}

// used to (dis|en)able Get Weight action button
sender.SetValueExt(package, "usrScaleRegistered", scaleRegistered);
}

protected virtual DateTime GetServerTime()
{
DateTime dbNow;
PXDatabase.SelectDate(out DateTime _, out dbNow);
dbNow = PXTimeZoneInfo.ConvertTimeFromUtc(dbNow, LocaleInfo.GetTimeZone());

return dbNow;
}

protected virtual decimal ConvertKilogramToWeightUnit(decimal weight, string weightUnit)
{
weightUnit = weightUnit.Trim().ToUpperInvariant();
decimal conversionFactor =
weightUnit == "KG" ? 1m :
weightUnit == "LB" ? 0.453592m :
throw new PXException(Msg.BoxWeightWrongUOM, weightUnit);

return weight / conversionFactor;
}
#endregion

#region Messages
[PXLocalizable]
public abstract class Msg
{
public const string ScaleMissing = "The scale is not found in the database.";
public const string ScaleDisconnected = "Scale data expired. Check scale connections.";
public const string ScaleTimeout = "Scale measurement has expired. Remove the package from the scale and try again.";
public const string BoxWeightWrongUOM = "Only KG and LB are supported.";
}
#endregion
}
User Profile Entry

 

Reply


About Acumatica ERP system
Acumatica Cloud ERP provides the best business management solution for transforming your company to thrive in the new digital economy. Built on a future-proof platform with open architecture for rapid integrations, scalability, and ease of use, Acumatica delivers unparalleled value to small and midmarket organizations. Connected Business. Delivered.
© 2008 — 2024  Acumatica, Inc. All rights reserved