How to get item attributes via api? - microsoft-dynamics

We are using cloud Dynamics 365 Business Central and trying to get items with all attributes via OData.
In Microsoft documentation we found this endpoint:
api.businesscentral.dynamics.com/v1.0[our tenant id]/Sandbox/ODataV4/Company('CRONUS%20DE')/items
But unfortunately, the response does not contain item attributes and values, such as Farbe, Tiefe, etc.
Next, we tried to add new Web Services. But some of this endpoints return empty values and some of them (7506, 7507, 7508, 7510) don't work and return:
No HTTP resource was found that matches the request URI
Objects 7500, 7501, 7503 contain information about attributes. But non of them (7500 - 7510) does not contain a relation between Item, Attributes, and Values.
Maybe there is another way to get items with their attribute values? We also tried to research microsoft graph but not successful.

i am having similar troubles with this. i find the dynamics api to be exceptionally unintuitive and difficult to use. the furthest i have been able to get has been to go into the api settings for dynamics and uncover the tables for a few item attributes (i believe that the table numbers are as those below:
7500 - Item Attribute
7501 - Item Attribute Value
7502 - Item Attribute Translation
7504 - Item Attribute Value Selection
7505 - Item Attribute Value Mapping
i cannot comment on why 7503 is missing.
using 7500 as an example, when you uncover the table, the system provides a resulting endpoint (unfortunately, they always promote OData, and the outdated SOAP resource; i can't figure out why they have such a vendetta against the simple and easy-to-use REST endpoint).
https://api.businesscentral.dynamics.com/v2.0/<TENANT_ID>/<ENVIRONMENT_NAME>/ODataV4/Company('COMPANY_IDENTIFIER')/ItemAttributes
using this endpoint, you can get a listing of the attribute types themselves (e.g. let's say you've defined an attribute called 'BaseColor', you should get a result here for the name of the attribute 'BaseColor', its ID, its type, etc.),
with the ItemAttributeValues endpoint, you should get the actual attribute values that are in existence (e.g. for some item, you happened to set its 'BaseColor' attribute to 'Blue', you should get a response for this attribute value with a attribute type of 'BaseColor', a value, as 'Blue' along with the entity's ID, etc).
yet, when it comes to any instantiated attribute values for items, i can't figure out how to get the association of the attributes with those items. i expect that the "item attribute value mapping" option would be something along the lines of a item_id - attribute_id pair so that for any item in question, one could query the attributes list with something like a filter. but as you said, upon uncovering some of these elements, their respective endpoints return nothing. you get to the point where you say 'OH...AWSOME! there is a value-item mapping. that makes sense, and i can definitely use that'. a few moments later, the API spits in your face with an error, or craps on you by returning something you don't expect like an empty data set.
this api is a constant uphill battle, and totally riddled with landmines. a complete blow-me-up-pain-in-the-arse.
EDIT: 2021-06-09
i've looked into this some more. i was able to set up an export package for the various tables in question, specifically 7500, 7501, and 7505. the magical table was 7505 as it is the relationship between an attribute value and the item with which it is associated. exporting the package to excel results in good data. yet, when trying to expose this information in the OData resource, something strange happens:
in web services, i try to open up page 7505 which populates the object name as ItemAttributeValueMapping and i set the service name to 'ItemAttributeValueMapping'. This is normal.
the system complains when i fail to specify that the object type is a page. so, i go back in the line and set the selection to "Page"
when i tab through to publish the change, the object name automatically changes to 'ItemAttributeValueTranslations'.
EDIT: 2021-06-15
After a lot of fiddling about, i finally reached a point where i decided that the only way to address this was to write an al query which would expose the appropriate value-item mapping information. there is a page which provides some source code for doing this:
github AL-Code-Samples
to get something out of the API, i had to use microsoft visual studio code. there are a few good videos on how to get this up and running to get a test app working for your business central instance (i used eric hougaar's videos: Getting started with Business Central AL Development).
when you have set up your app to connect to your instance by inserting your tenant and entering your credentials, you can modify the source code as below to create a query in your system.
query 50102 "<YOUR_QUERY_NAME>"
{
QueryType = API;
APIPublisher = '<YOUR_NAME>';
APIGroup = '<YOUR_APP_GROUP>';
APIVersion = 'v1.0';
Caption = '<YOUR CAPTION>';
EntityName = '<YOUR_QUERY_NAME>';
EntitySetName = '<YOUR_API_ENDPOINT_NAME>';
elements
{
dataitem(Item; Item)
{
column(No_; "No.")
{
}
column(Description; Description)
{
}
column(Unit_Cost; "Unit Cost")
{
}
dataitem(Item_Attribute_Value_Mapping; "Item Attribute Value Mapping")
{
DataItemLink = "No." = Item."No.";
column(Item_Attribute_ID; "Item Attribute ID")
{
}
column(Item_Attribute_Value_ID; "Item Attribute Value ID")
{
}
dataitem(QueryElement6; "Item Attribute")
{
DataItemLink = ID = Item_Attribute_Value_Mapping."Item Attribute ID";
column(Name; Name)
{
}
dataItem(Queryelement10; "Item Attribute Value")
{
DataItemLink = "Attribute ID" = Item_Attribute_Value_Mapping."Item Attribute ID",
ID = Item_Attribute_Value_Mapping."Item Attribute Value ID";
column(Value; Value)
{
}
column(Numeric_Value; "Numeric Value")
{
}
}
}
}
}
}
}
once this code gets successfully uploaded to your server and returning a page (you have to wait for it), you can then use specified query number to expose the data in the API by going to business central's "web services" and adding a 'Query' to item 50102 (or whatever number you use). the endpoint will automatically be populated and you can use it to send you back the necessary JSON which will show a product, with its attribute values.
hope that helps.

You should try with below endpoint:
/v2.0/tenant_id/enviornment_name/ODataV4/Company(company_id)/Items

Related

Use a view to get the same suffix like maindomain name

I have some documents, how can use a view to get the document which have the same domain name for their email address. like all the document with #gmail.com or #yahoo.com, if endkey can get that results?
Here is what I wrote a view on map, But I do not think this is good idea
function(doc) {
for (var i in doc.emails) {
if (doc.emails[i].emailAddress.toLowerCase().indexOf("#yahoo.ibm.com")!=-1) {
emit(doc.emails[i].emailAddress.toLowerCase(), doc);
}
}
}
}
To make things clear, the endkey parameter is not looking for a suffix. Startkey and endkey are like the limits of keys to get. For example, you could get the document with the id 1 to the id 10 startkey="1"&endkey="10" .
In your case, you want to make a view that will group your documents by their domain name. I created a design document with a byDomain view. The mapping function looks like this :
function(doc){
if(doc.email){ //I used the document's property email for my view.
//Now, we will emit an array key. The first value will be the domain.
//To get the domain, we split the string with the character '#' and we take what comes after.
//Feel free to add more validations
//The second key will be the document id. We don't emit any values. It's faster to simply add
//the includes_docs query parameter.
emit([doc.email.split('#')[1],doc._id]);
}
}
Let's query all my documents to show you what I have
Request : http://localhost:5984/test/_all_docs?include_docs=true
Response:
{"total_rows":4,"offset":0,"rows":[
{"id":"7f34ec3b9332ab4e555bfca202000e5f","key":"7f34ec3b9332ab4e555bfca202000e5f","value":{"rev":"1-c84cf3bf33e1d853f99a4a5cb0a4af74"},"doc":{"_id":"7f34ec3b9332ab4e555bfca202000e5f","_rev":"1-c84cf3bf33e1d853f99a4a5cb0a4af74","email":"steve#gmail.com"}},
{"id":"7f34ec3b9332ab4e555bfca202001101","key":"7f34ec3b9332ab4e555bfca202001101","value":{"rev":"1-53a8a9f2a24d812fe3c98ad0fe020197"},"doc":{"_id":"7f34ec3b9332ab4e555bfca202001101","_rev":"1-53a8a9f2a24d812fe3c98ad0fe020197","email":"foo#example.com"}},
{"id":"7f34ec3b9332ab4e555bfca202001b02","key":"7f34ec3b9332ab4e555bfca202001b02","value":{"rev":"1-cccec02fe7172fb637ac430f0dd25fa2"},"doc":{"_id":"7f34ec3b9332ab4e555bfca202001b02","_rev":"1-cccec02fe7172fb637ac430f0dd25fa2","email":"bar#gmail.com"}},
{"id":"_design/emails","key":"_design/emails","value":{"rev":"4-76785063c7dbeec96c495db76a8faded"},"doc":{"_id":"_design/emails","_rev":"4-76785063c7dbeec96c495db76a8faded","views":{"byDomain":{"map":"\t\tfunction(doc){\n\t\t\tif(doc.email){ //I used the document's property email for my view.\n\t\t\t\t//Now, we will emit an array key. The first value will be the domain.\n\t\t\t\t//To get the domain, we split the string with the character '#' and we take what comes after.\n\t\t\t\t//Feel free to add more validations\n\t\t\t\t//The second key will be the document id. We don't emit any values. It's faster to simply add\n\t\t\t\t//the includes_docs query parameter.\n\t\t\t\temit([doc.email.split('#')[1],doc._id]); \n\t\t\t}\n\t\t}"}},"language":"javascript"}}
]}
As you can see, I got few minimalist documents with the property "email" set.
Let's query my view without any parameters
Request : http://localhost:5984/test/_design/emails/_view/byDomain
Response :
{"total_rows":3,"offset":0,"rows":[
{"id":"7f34ec3b9332ab4e555bfca202001101","key":["example.com","7f34ec3b9332ab4e555bfca202001101"],"value":null},
{"id":"7f34ec3b9332ab4e555bfca202000e5f","key":["gmail.com","7f34ec3b9332ab4e555bfca202000e5f"],"value":null},
{"id":"7f34ec3b9332ab4e555bfca202001b02","key":["gmail.com","7f34ec3b9332ab4e555bfca202001b02"],"value":null}
]}
Let's query only documents with that have the gmail.com domain.
Request : http://localhost:5984/test/_design/emails/_view/byDomain?startkey=["gmail.com"]&endkey=["gmail.com","\ufff0"]
Result :
{"total_rows":3,"offset":1,"rows":[
{"id":"7f34ec3b9332ab4e555bfca202000e5f","key":["gmail.com","7f34ec3b9332ab4e555bfca202000e5f"],"value":null},
{"id":"7f34ec3b9332ab4e555bfca202001b02","key":["gmail.com","7f34ec3b9332ab4e555bfca202001b02"],"value":null}
]}
You can just use a simple map function for this:
function (doc) {
var domain = doc.email.split('#').pop();
// this logic is fairly hack-ish, you may want to be more sophisticated
emit(domain);
}
Then you can simply pass key=gmail.com to get the results you want from the view. I would also add include_docs=true instead of emitting the entire document as your value.
You can read more about views in the official CouchDB docs.

Create NewRecord through Siebel Business Service eScript

I am trying to create a new record using BS server script.
Since the process is taking place inside the BS, the context of Parent is not present, hence I am unable to get Parent Row_Id which I need to explicitly stamp against the child record being created for visibility.
Initially I tried to pass the Parent Row_Id from applet as a profile, but this fails when there are no records in the child applet, ie this.BusComp().ParentBusComp().GetFieldValue returns "This operation is invalid when there are no records present" as the "this" context is unavailable.
Any suggestions?
I was able to achieve the desired with the below code
sId = TheApplication().ActiveBusObject().GetBusComp("Q").ParentBusComp().GetFieldValue("Id");
if(this.BusComp().CountRecords() > 0)
{
sA = TheApplication().ActiveBusObject().GetBusComp("Q").GetFieldValue("A");
sB = TheApplication().ActiveBusObject().GetBusComp("Q").GetFieldValue("B");
}
sEntity = TheApplication().ActiveBusObject().GetBusComp("Q").Name();
It is for these reasons that Siebel provides Pre-Default settings at the Business Component Field level. If you wish to do this entirely through scripting, you will have to find the Active context, you have to know which BC is the parent.
Lets say you know that the Parent BC has to be Account. So
ActiveBusObject().GetBusComp("Account").GetFieldValue("Id") will give you the row id of the currently selected Account BC record. But do make sure that this script fires only in this context. So check the ActiveViewName to check this.
if(TheApplication().GetProfileAttr("ActiveViewName")=="Custom View")
{
//put the scripting here.
}

Where to put code from another JS library in Ember: controller, model, adapter, service?

Wrapping another javascript library to use with Ember bindings, etc, seems like an ordinary thing to do, but I haven't found much discussion of it.
I want to filter an ember record array using distance and travel time from the Google Maps Distance Matrix
service. I'm just not sure where in the application to encapsulate Google's javascript. Note: this is not a question about embedding a google map, it's about getting data into ember that doesn't come from a rest/json or fixtures as in all the tutorials and examples I've found.
Would people typically do this in the controller or create new models/adapters to get benefits from store caching? Or is there another way?
update: in case that's too vague, consider this: 20 records (with a google map component etc) listed by an array controller, a text field where the user types in a home address, a couple of other inputs where they set a maximum time or distance, and a search button which filters the listed records by comparing the user requirements with the result of querying the distance matrix for the home address to the 20 records' addresses, only showing the ones close enough to their home.
Use of the service in an application that doesn't display a Google map is prohibited.
So,the question is really about integrating a Google map to an Ember app.
Without any doubt you'll have to add the Google JS like in any other HTML project with:
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=MYSECRETKEY"></script>
So, the API is in global space and you just use it whenever you need it. Mostly all that will happen in your views, so you could wrap everything in a component. (I'm assuming that all relevant data has been passed from the controller to the view, it all depends on the design of your app.)
The following works, but it seems like it should be in the model/store/adapter layer.
App.DistanceController = Ember.Controller.extend
origin: (->
data = #get('data')
data.origin if data
).property('data')
destinationDistances: (->
data = #get('data')
data.distances if data
).property('data')
data: ((key, value)->
if arguments.length > 1
value
else
_this = this
value = null
service = new google.maps.DistanceMatrixService()
service.getDistanceMatrix(
origins: ["London, England"],
destinations: [
"Bristol, England",
"Edinburgh, Scotland",
"Leeds, England",
"Liverpool, England",
"Manchester, England",
"Newcastle, England",
"Nottingham, England",
"York, England"
],
travelMode: google.maps.TravelMode.DRIVING,
avoidHighways: false,
avoidTolls: false
, (response, status) ->
if (status == google.maps.DistanceMatrixStatus.OK)
distances = []
for destination, n in response.destinationAddresses
distances.push {
destination: destination
distance: response.rows[0].elements[n].distance.text
}
_this.set('data', {
origin: response.originAddresses[0]
distances: distances
})
)
value
).property()
kudos #rlivsey https://stackoverflow.com/a/20623551/395180

Sitecore Multisite Manager and 'source' field in template builder

Is there any way to parametise the Datasource for the 'source' field in the Template Builder?
We have a multisite setup. As part of this it would save a lot of time and irritation if we could point our Droptrees and Treelists point at the appropriate locations rather than common parents.
For instance:
Content
--Site1
--Data
--Site2
--Data
Instead of having to point our site at the root Content folder I want to point it at the individual data folders, so I want to do something like:
DataSource=/sitecore/content/$sitename/Data
I can't find any articles on this. Is it something that's possible?
Not by default, but you can use this technique to code your datasources:
http://newguid.net/sitecore/2013/coded-field-datasources-in-sitecore/
You could possibly use relative paths if it fits with the rest of your site structure. It could be as simple as:
./Data
But if the fields are on random items all over the tree, that might not be helpul.
Otherwise try looking at:
How to use sitecore query in datasource location? (dynamic datasouce)
You might want to look at using a Querable Datasource Location and plugging into the getRenderingDatasource pipeline.
It's really going to depend on your use cases. The thing I like about this solution is there is no need to create a whole bunch of controls which effectively do he same thing as the default Sitecore ones, and you don't have to individually code up each datasource you require - just set the query you need to get the data. You can also just set the datasource query in the __standard values for the templates.
This is very similar to Holger's suggestion, I just think this code is neater :)
Since Sitecore 7 requires VS 2012 and our company isn't going to upgrade any time soon I was forced to find a Sitecore 6 solution to this.
Drawing on this article and this one I came up with this solution.
public class SCWTreeList : TreeList
{
protected override void OnLoad(EventArgs e)
{
if (!String.IsNullOrEmpty(Source))
this.Source = SourceQuery.Resolve(SContext.ContentDatabase.Items[ItemID], Source);
base.OnLoad(e);
}
}
This creates a custom TreeList control and passes it's Source field through to a class to handle it. All that class needs to do is resolve anything you have in the Source field into a sitecore query path which can then be reassigned to the source field. This will then go on to be handled by Sitecore's own query engine.
So for our multi-site solution it enabled paths such as this:
{A588F1CE-3BB7-46FA-AFF1-3918E8925E09}/$sitename
To resolve to paths such as this:
/sitecore/medialibrary/Product Images/Site2
Our controls will then only show items for the correct site.
This is the method that handles resolving the GUIDs and tokens:
public static string Resolve(Item item, string query)
{
// Resolve tokens
if (query.Contains("$"))
{
MatchCollection matches = Regex.Matches(query, "\\$[a-z]+");
foreach (Match match in matches)
query = query.Replace(match.Value, ResolveToken(item, match.Value));
}
// Resolve GUIDs.
MatchCollection guidMatches = Regex.Matches(query, "^{[a-zA-Z0-9-]+}");
foreach (Match match in guidMatches)
{
Guid guid = Guid.Parse(match.Value);
Item queryItem = SContext.ContentDatabase.GetItem(new ID(guid));
if (item != null)
query = query.Replace(match.Value, queryItem.Paths.FullPath);
}
return query;
}
Token handling below, as you can see it requires that any item using the $siteref token is inside an Site Folder item that we created. That allows us to use a field which contains the name that all of our multi-site content folders must follow - Site Reference. As long at that naming convention is obeyed it allows us to reference folders within the media library or any other shared content within Sitecore.
static string ResolveToken(Item root, string token)
{
switch (token)
{
case "$siteref":
string sRef = string.Empty;
Item siteFolder = root.Axes.GetAncestors().First(x => x.TemplateID.Guid == TemplateKeys.CMS.SiteFolder);
if (siteFolder != null)
sRef = siteFolder.Fields["Site Reference"].Value;
return sRef;
}
throw new Exception("Token '" + token + "' is not recognised. Please disable wishful thinking and try again.");
}
So far this works for TreeLists, DropTrees and DropLists. It would be nice to get it working with DropLinks but this method does not seem to work.
This feels like scratching the surface, I'm sure there's a lot more you could do with this approach.

Can't access a new field programmatically on a template in Sitecore

my question is basically the same as #Bob Black's Cannot access sitecore item field via API but I agree with #techphoria414 that the accepted solution is not necessary and in my case does not work.
In my own words, I have a template Departure that I have been using for about a year now creating and updating items programmatically. I have added a new field Ship to the template. When I create a new item the field comes up as null when I try to access it using departure.Fields["Ship"]. If I step over the line causing the exception then after calling departure.Editing.EndEdit() I can then see the Ship field if I call departure.Fields.ToList(). If I add the template to a content item via the Sitecore GUI I can see the field and use it, and if I look at a content item which is based on the template I can see the new field too. So it is only when I access the template/item programmatically that it is null.
I have sitecore running on my local machine with a local sqlserver, and publish to my local machine.
Here is my code
String ship = "MSDisaster";
foreach (Language language in SiteLanguages)
{
departure = departure.Database.GetItem(departure.ID, language);
departure.Editing.BeginEdit();
try
{
departure.Fields["StartDate"].Value = GetSitecoreDateString(xDep, "StartDate");
departure.Fields["EndDate"].Value = GetSitecoreDateString(xDep, "EndDate");
departure.Fields["Guaranteed"].Value = xDep.SelectSingleNode("./Guaranteed").InnerText;
departure.Fields["Status"].Value = xDep.SelectSingleNode("./Status").InnerText;
departure.Fields["Currency"].Value = ConvertLanguageToCurrency(language);
departure.Fields["Market"].Value = ConvertLanguageToMarket(language);
departure.Fields["TwinSharePrice"].Value = GetPrice(xDep, "twn", language);
departure.Fields["SinglePrice"].Value = GetPrice(xDep, "sgl", language);
if (!String.IsNullOrEmpty(ship))
departures.Fields["Ship"].Value = ship;
}
catch (Exception ex)
{
departure.Editing.CancelEdit();
log.Error(ex);
throw ex;
}
departure.Editing.EndEdit();
}
So, how do I get the field be picked up?
Thanks,
James.
Firstly do you see the field in the web database in the sitecore administration.
If you do the item has the fields, you then should check the template assigned on the item and double check that the field is actually called "ship" and check the case as ive seen this as an issue before.
Also check the security on the item and field just in case anyone changed anything.
Next try and get the data from the item but instead of using the field name, use the field ID.
Let me know how you go?
Chris
Sorry Chris, StackOverflow, and the others who looked at my questions. It was a stupid typo. It's even there in my question
departure.Fields["SinglePrice"].Value = GetPrice(xDep, "sgl", language);
if (!String.IsNullOrEmpty(ship))
departures.Fields["Ship"].Value = ship;
}
departure is the item I am working on, departures is the collection it belongs to... doh.
So what is the protocol here? Do I delete my question now because it isn't really going to help anyone code better?
James.