Het toepassen van prijsextensies in Google Ads accounts van grotere ecommerce partijen is een tijdrovende klus. Wanneer prijzen in de shop aangepast worden, moet dit handmatig doorgevoerd worden in een prijsextensie. Of je kiest ervoor om dit middels een CSV-upload gedeeltelijk te automatiseren, maar dat betekent nog steeds dat je dagelijks een nieuwe sheet moet uploaden.
Met onderstaand script kan je bestaande prijsextensies gemakkelijk controleren. Wanneer prijzen in de shop veranderen, past het script de nieuwe prijs ook toe in de prijsextensie. Je kan het script gebruiken voor prijsextensies met productprijzen, categorieën en merken. Door een koppeling met Google Merchant Center (GMC), heeft het script altijd de meest actuele prijzen om toe te kunnen passen in de prijsextensies.
Producten, categorieën en merken
De toepassing van het script op productprijzen is eenvoudig; op basis van de product-URL wordt de laatste prijs in GMC gezocht. Wanneer een product een andere prijs heeft, wordt deze aangepast in de prijsextensie. Wanneer een product niet meer in GMC staat, wordt het product verwijderd uit de prijsextensie.
Wanneer je prijzen van merken in de shop in een prijsextensie zet, zoekt het script alle producten met dit merk en pakt dan de laagste prijs uit GMC. Hierdoor heb je altijd een actuele 'vanaf-prijs'. Zorg er voor dat de merknaam die in de titel van het prijsitem van de extensie staat, overeenkomt met de merknaam in GMC. Voor categorieën geldt hetzelfde als voor merken. Wanneer een categorie in de titel van een prijsitem overeenkomst met een veld uit GMC (dit veld kan je in het script zelf bepalen), zoekt het script de laagste prijs om in de prijsextensie op te nemen.
Wanneer een item uit een prijsextensie verwijderd moet worden, maar de prijsextensie heeft maar drie items, wordt er een einddatum op de prijsextensie ingesteld. Je ontvangt dan ook een mail dat er een prijsextensie is gepauzeerd, zodat je weet dat je een nieuwe moet aanmaken of in de bestaande prijsextensie een nieuw item moet toevoegen.
Zorg ervoor dat je dit script voor gebruik uitvoerig in voorbeeld-modus draait om te kijken welke wijzigingen gemaakt zouden worden. Controleer daarbij of de prijzen overeenkomen met Google Merchant Center.
Instellingen
Met de volgende instellingen kan je het script toepassen naar wens:
LOG: Geef aan of het script de tussenstappen moet rapporteren, door de waarde aan te passen naar 'true'.
MERCHANT_ID: Schakel de geavanceerde API 'Shopping Content' in en vul hier het Merchant Center ID in. Zorg ervoor dat de gebruiker die het script draait ook toegang heeft tot GMC.
INCLUDE_PRODUCT_TIERS: Zet op 'true' wanneer je prijsextensies met productprijzen wilt controleren, anders zet je deze op 'false'.
INCLUDE_BRANDS: Zet op 'true' wanneer je prijsextensies met merkprijzen wilt controleren, anders zet je deze op 'false'.
INCLUDE_CATEGORIES: Zet op 'true' wanneer je prijsextensies met categorieprijzen wilt controleren, anders zet je deze op 'false'.
CATEGORY_FEEDFIELD: Wanneer je prijsextensies met categorieprijzen wilt controleren, kan je hier aangeven welk veld in GMC de categorie bevat.
EMAIL_RECIPIENTS: Wanneer prijsextensies gepauzeerd worden, wordt er een mail verstuurd naar deze ontvanger(s).
EMAIL_SUBJECT: Geef het onderwerp van de email aan
EMAIL_CONTENT: Inhoud van de email, welke wordt aangevuld met meldingen uit het account.
The script
// Copyright 2021
//
// Created By: Tibbe van Asten
// for Increase B.V.
//
// Created 15-08-2019
// Last update: 08-10-2021 - Changes productType to productTypes
//
// ABOUT THE SCRIPT
// This script will check all price-extensions for up-to-date prices
// When prices have changed, the script will update the extension.
//
////////////////////////////////////////////////////////////////////
var config = {
LOG : true,
// Connect Merchant Center. Add user to MC that runs this script.
// Also enable Advanced API 'Shopping content'.
MERCHANT_ID : "123456789",
INCLUDE_PRODUCT_TIERS : true,
INCLUDE_BRANDS : true,
INCLUDE_CATEGORIES : true,
// Specify the field in Merchant Center that contains your product-category
CATEGORY_FEEDFIELD : "productTypes",
// An email will be send when price extensions are paused
// Add multiple emailaddresses separated by a comma.
EMAIL_RECIPIENTS : "example@example.com",
EMAIL_SUBJECT : "Update Price Extensions in " + AdsApp.currentAccount().getName(),
EMAIL_CONTENT : "The following changes are made in price extensions:<br /><br />"
}
////////////////////////////////////////////////////////////////////
function main(){
// Connect Merchant Center and collect active brands and products
var products = connectMerchant();
var newPriceItems = [];
// Set builders
var priceItemBuilder = AdsApp.extensions().newPriceItemBuilder();
var priceBuilder = AdsApp.extensions().newPriceBuilder();
var priceIterator = AdsApp
.extensions()
.prices()
.withCondition("Status = ENABLED")
.withCondition("PolicyApprovalStatus IN ['APPROVED','APPROVED_LIMITED']")
.get();
while(priceIterator.hasNext()){
var price = priceIterator.next();
var priceItems = price.getPriceItems();
var numItems = priceItems.length;
// Productprices are checked, when the price extension includes products
// and config is set to include product tiers
if(config.INCLUDE_PRODUCT_TIERS === true && !price.getEndDate() && price.getPriceType() == "PRODUCT_TIERS"){
for(var i = 0;i < priceItems.length;i++){
var priceItem = priceItems[i];
var currentPrice = checkPrice(products, priceItem.getFinalUrl());
numItems = checkPriceItem(currentPrice, price, priceItems, priceItem, numItems, priceItemBuilder);
} // priceItemIterator
} // PRODUCT_TIERS
// Brandprices are checked, when the price extension includes brands
// and config is set to include brand prices
if(config.INCLUDE_BRANDS === true && !price.getEndDate() && price.getPriceType() == "BRANDS"){
for(var i = 0;i < priceItems.length;i++){
var priceItem = priceItems[i];
var currentPrice = checkBrandPrice(products, priceItem.getHeader());
numItems = checkPriceItem(currentPrice, price, priceItems, priceItem, numItems, priceItemBuilder);
} // priceItemIterator
} // BRANDS
// ProductCategories are checked, when the price extension includes categories
// and config is set to include product categories
if(config.INCLUDE_CATEGORIES === true && !price.getEndDate() && price.getPriceType() == "PRODUCT_CATEGORIES"){
for(var i = 0;i < priceItems.length;i++){
var priceItem = priceItems[i];
var currentPrice = checkCategoryPrice(products, priceItem.getHeader());
numItems = checkPriceItem(currentPrice, price, priceItems, priceItem, numItems, priceItemBuilder);
} // priceItemIterator
} // PRODUCT_CATEGORIES
} // priceIterator
sendEmail();
Logger.log("Thanks for using this custom script by Tibbe van Asten. Winning!");
} // function main
////////////////////////////////////////////////////////////////////
function connectMerchant(){
if(config.MERCHANT == "123456789"){
throw Error("Change the Merchant ID in the settings");
}
var pageToken;
var pageNum = 1;
var maxResults = 250;
var products = [];
do {
var productList = ShoppingContent.Products.list(config.MERCHANT_ID, {
pageToken: pageToken,
maxResults: maxResults
});
if (productList.resources) {
for (var i = 0; i < productList.resources.length; i++) {
// We'll only check for products that are in stock
if(productList.resources[i]["availability"] == "in stock"){
products.push(productList.resources[i]);
}
}
}
pageToken = productList.nextPageToken;
pageNum++;
} while (pageToken);
return products;
} // function connectMerchant()
////////////////////////////////////////////////////////////////////
function checkPrice(products, url){
var productPrice = 1000;
for (var i = 1; i < products.length; i++) {
// When the link in the feed is the same as the price-item url, the price is retrieved
if(products[i]["link"] == url) {
// When the saleprice is in the feed, this will be used. If not, the regular price is used.
if(products[i]["salePrice"]) {
var price = products[i]["salePrice"]["value"];
} else {
var price = products[i]["price"]["value"];
}
if(price < productPrice){
productPrice = price;
}
} // if statement url
} // for statement
if(productPrice != 1000){
return productPrice;
}
} // function checkPrice()
////////////////////////////////////////////////////////////////////
function checkBrandPrice(products, brand){
var brandPrice = 1000;
for (var i = 1; i < products.length; i++) {
// When the brand in the feed is the same as the price-item brand, the price is retrieved
if(products[i]["brand"] == brand) {
// When the saleprice is in the feed, this will be used. If not, the regular price is used.
if(products[i]["salePrice"]) {
var price = parseFloat(products[i]["salePrice"]["value"]);
} else {
var price = parseFloat(products[i]["price"]["value"]);
}
if(price < brandPrice){
brandPrice = price;
}
} // if statement brand
} // for statement
if(brandPrice != 1000){
return brandPrice;
}
} // function checkBrandPrice()
////////////////////////////////////////////////////////////////////
function checkCategoryPrice(products, category){
var categoryPrice = 1000;
for (var i = 1; i < products.length; i++) {
// When the brand in the feed is the same as the price-item productType, the price is retrieved
if(products[i][config.CATEGORY_FEEDFIELD] == category) {
// When the saleprice is in the feed, this will be used. If not, the regular price is used.
if(products[i]["salePrice"]) {
var price = parseFloat(products[i]["salePrice"]["value"]);
} else {
var price = parseFloat(products[i]["price"]["value"]);
}
if(price < categoryPrice){
categoryPrice = price;
}
} // if statement brand
} // for statement
if(categoryPrice != 1000){
return categoryPrice;
}
} // function checkCategoryPrice()
////////////////////////////////////////////////////////////////////
function checkPriceItem(currentPrice, price, priceItems, priceItem, numItems, priceItemBuilder){
// If product is not found, the priceItem will be removed.
// When price only exists of 3 items, the price will be removed
if(currentPrice == null && numItems > 3){
priceItem.remove();
numItems--;
// Summary changes in mail
config.EMAIL_CONTENT += "Removed " + priceItem.getHeader() + " from price #" + price.getId() + "<br />";
if(config.LOG === true){
Logger.log("Removed " + priceItem.getHeader() + " from price #" + price.getId());
Logger.log(" ");
}
} // remove priceItem, not in feed
else if(currentPrice == null && numItems == 3){
var date = new Date(Utilities.formatDate(new Date(), 'Europe/Amsterdam', 'yyyy/MM/dd HH:mm').toString());
if(price.getEndDate() == null){
price.setEndDate({year: date.getYear(), month: date.getMonth()+1, day: date.getDate()})
price.setSchedules([{dayOfWeek: "MONDAY", startHour: 0, startMinute: 0, endHour: date.getHours(), endMinute: 0}]);
if(config.LOG === true){
Logger.log("Ended " + priceItem.getHeader() + " in #" + price.getId());
Logger.log(" ");
}
// Summary changes in mail
config.EMAIL_CONTENT += "Ended " + priceItem.getHeader() + " in #" + price.getId() + "<br />";
}
} // remove price, not in feed anymore
// If price from priceItem is not the same as the one in the feed
// we'll remove it.
if(priceItem.getAmount() != currentPrice && currentPrice != null){
Logger.log(priceItem.getHeader() + ". Current: " + priceItem.getAmount() + ", New: " + currentPrice);
// Add dummy item
var dummyPriceItemOperation = priceItemBuilder
.withHeader("Dummy")
.withDescription("Dummy")
.withAmount(10)
.withCurrencyCode(priceItem.getCurrencyCode())
.withUnitType("NONE")
.withFinalUrl("http://www.example.com/")
.build();
// Setting up new priceItem
var newPriceItemOperation = priceItemBuilder
.withHeader(priceItem.getHeader())
.withDescription(priceItem.getDescription())
.withAmount(parseFloat(currentPrice))
.withUnitType(priceItem.getUnitType())
.withCurrencyCode(priceItem.getCurrencyCode())
.withFinalUrl(priceItem.getFinalUrl())
.build();
var dummyPriceItem = dummyPriceItemOperation.getResult();
var newPriceItem = newPriceItemOperation.getResult();
// Adding dummyPriceitem and removing original
price.addPriceItem(dummyPriceItem);
priceItem.remove();
price.addPriceItem(newPriceItem);
if(config.LOG === true){
Logger.log("Renewed " + priceItem.getHeader());
Logger.log(" ");
}
// Summary changes in mail
config.EMAIL_CONTENT += "Renewed " + priceItem.getHeader() + "<br />";
} // Replace item with wrong price
// Remove dummy priceItem
var priceItems = price.getPriceItems()
for(var y = 0;y < priceItems.length;y++){
var priceItem = priceItems[y];
if(priceItem.getHeader() == "Dummy"){
priceItem.remove();
}
} // Remove dummy
return numItems;
} // function checkPriceItem()
////////////////////////////////////////////////////////////////////
function sendEmail() {
if(config.EMAIL_CONTENT != "The following changes are made in price extensions:<br /><br />"){
MailApp.sendEmail({
to: config.EMAIL_RECIPIENTS,
subject: config.EMAIL_SUBJECT,
htmlBody: config.EMAIL_CONTENT});
Logger.log(config.EMAIL_CONTENT);
}
} // function sendEmail()
Show whole script!
Loading Comments
The Experts
Tibbe van AstenTeam Lead Performance Marketing
Nils RooijmansWater Cooler Topics
Martijn KraanFreelance PPC Specialist
Bas BaudoinTeamlead SEA @ Happy Leads
Jermaya LeijenDigital Marketing Strategist
Krzysztof BycinaPPC Specialist from Poland
How about you?JOIN US!
Caring
Adsscripts.com staat voor het delen van kennis. In de huidige markt houden SEA-specialisten de kennis en ervaring graag voor zich. Wij zijn er van overtuigd dat het delen van kennis ervoor kan zorgen dat iedereen beter wordt in haar of zijn werk. Daarom lopen wij hier graag in voorop, door onze kennis over scripts te delen met iedereen.
Wil jij ook graag een bijdrage leveren? Wij staan open voor nieuwe ideeën en feedback op alles wat je op Adsscripts.com vindt.