ããã«ã¡ã¯ãã¬ãã³ã ã®ã³ãŒãã¬ãŒããšã³ãžãã¢ãªã³ã°ããŒã ã® @ken-1200 ã§ãã ãã®èšäºã¯ã RevComm Advent Calendar 2024 ã® 20 æ¥ç®ã®èšäºã§ãã 1. ã¯ããã« 2. éçºã®èæ¯ã»ã¢ãããŒã·ã§ã³ 3. åææ¡ä»¶ 4. Salesforce CPQ APIã®æŠèŠ 5. åè«ïŒOpportunityïŒã®äœæ 6. èŠç©ïŒQuoteïŒã®äœæ 7. èŠç©åç®ïŒQuote Line ItemsïŒã®ç»é² 8. ãã€ã³ãã®æ¯ãè¿ã 9. èªååã«ããåŸããã广 10. èŠåŽããç¹ã»ãããã©ãã 11. ãŸãšã 12. åèæç® 1. ã¯ããã« èšäºã®ç®ç æ¬èšäºã§ã¯ãSalesforce CPQ APIãæŽ»çšããŠãåè«ããèŠç©äœæãèŠç©åç®ã®ç»é²ãŸã§ã®ããã»ã¹ãèªååããæé ãã玹ä»ããŸã 察象èªè
Salesforce CPQãå©çšãããšã³ãžãã¢ã®æ¹ãäž»ãªèªè
ãšæ³å®ããŠããŸã 2. éçºã®èæ¯ã»ã¢ãããŒã·ã§ã³ ãªãéçºã«è³ã£ãã®ã å¶æ¥ããã»ã¹ã§ã¯ãåè«ã»èŠç©ã»èŠç©åç®ãªã©ã®æ
å ±å
¥åãä¿®æ£ãæåã§è¡ãããå·¥æ°ãããã£ãŠããŸãããããã«ããªã³ã©ã€ã³ç³èŸŒå¯Ÿå¿æã«ã¯ãååã®è¿œå ã»åé€ã»äŸ¡æ Œèª¿æŽãéœåºŠè¡ãå¿
èŠããããæäœæ¥ã«ãããã¹ãäœæ¥é
å»¶ãçºçããã¡ã§ãã ãããã®èª²é¡è§£æ±ºã®ãããSalesforce CPQ APIãçšããèªååã«ãã£ãŠãæ¥åãããŒãå¹çåããæ£ç¢ºæ§ãšã¹ããŒãã®åäžãç®æããŸãã 3. åææ¡ä»¶ Salesforceç°å¢ã®æºå Salesforce CPQãæå¹åãããŠããããš å¿
èŠãªAPIã¢ã¯ã»ã¹æš©éïŒãŠãŒã¶ãŒæš©éèšå®ïŒãèšå®ãããŠããããš éçºç°å¢ã®ã»ããã¢ãã Salesforce Sandboxç°å¢ ããã°ã©ãã³ã°èšèªïŒPython 3.10以äžãæšå¥šããŸã åºæ¬çãªç¥è Salesforce CPQã®åºæ¬æŠå¿µ REST APIã®åºç€ç¥è 4. Salesforce CPQ APIã®æŠèŠ APIã®çš®é¡ REST APIãšSOAP APIã®2çš®é¡ãååšããŸããã軜éã§æ±çšæ§ãé«ããJSON圢åŒã®ããŒã¿äº€æã容æãªREST APIãéžæããŸã èªèšŒæ¹æ³ äžè¬çã«ã¯OAuth 2.0ã䜿çšããããšã§ãããŒã¯ã³ããŒã¹ã®èªèšŒã»èªå¯ãå¯èœã§ã ãšã³ããã€ã³ããšãªãœãŒã¹ Salesforce CPQã«ã¯ç¹å®ã®ãšã³ããã€ã³ããä»ããŠèŠç©ãååæ
å ±ãžã¢ã¯ã»ã¹ã§ããŸãã以äžã¯äž»ãªäŸã§ã QuoteReader : /services/apexrest/SBQQ/ServiceRouter?reader=SBQQ.QuoteAPI.QuoteReader&uid={quote_id} æå®ããQuote IDã®è©³çްæ
å ±ãååŸããŸã ProductLoader : /services/apexrest/SBQQ/ServiceRouter?loader=SBQQ.ProductAPI.ProductLoader&uid={product_id} æå®ããååIDã«å¯Ÿããååæ
å ±ããªãã·ã§ã³ãååŸããŸã QuoteProductAdder : /services/apexrest/SBQQ/ServiceRouter?loader=SBQQ.QuoteAPI.QuoteProductAdder èŠç©ã«ååã远å ããããã«äœ¿çšããŸã QuoteCalculator : /services/apexrest/SBQQ/ServiceRouter?loader=SBQQ.QuoteAPI.QuoteCalculator èŠç©åç®ã远å åŸã«ãèŠç©å
šäœã®äŸ¡æ Œèšç®ãè¡ããŸã QuoteSave : /services/apexrest/SBQQ/ServiceRouter èšå®ããèŠç©ãèŠç©åç®ãä¿åããã³ç¢ºå®ããŸã ãããã®ãšã³ããã€ã³ããçµã¿åãããããšã§ãåè«äœæâèŠç©çæâèŠç©åç®è¿œå âäŸ¡æ Œåèšç®âä¿åãšããäžé£ã®æµããèªååã§ããŸã SalesforceRestApiClientã¯ã©ã¹ã«ã€ã㊠Salesforce CPQ APIãSalesforceæšæºAPIãžã®ã¢ã¯ã»ã¹ãç°¡æœã«ããããã«ãæ¬èšäºã§ã¯å
±éçã«å©çšã§ãã SalesforceRestApiClient ã¯ã©ã¹ãçšããŠããŸã from collections.abc import Mapping from typing import Any import httpx class SalesforceRestApiClient : """Salesforce REST APIã¯ã©ã€ã¢ã³ãã¯ã©ã¹ Salesforce APIãåŒã³åºãããã®åºæ¬ã¯ã©ã¹ã§ã """ def __init__ (self, path: str , additional_headers: dict | None = None ): self.base_url = "https://your-instance.salesforce.com" # Salesforceã€ã³ã¹ã¿ã³ã¹URLãæå®ããŠãã ãã self.path = path # ããã§ã¯äŸãšããŠAuthorizationããããçç¥ããŠããŸããã # å®éã«ã¯OAuth2ããŒã¯ã³ãæå¹ãªèªèšŒããããèšå®ããŠãã ãã self.headers = { "Authorization" : "Bearer your_access_token" , "Content-Type" : "application/json" } if additional_headers: self.headers.update(additional_headers) async def get (self) -> httpx.Response: """GETãªã¯ãšã¹ããéä¿¡ããŸã""" async with httpx.AsyncClient() as client: return await client.get(f "{self.base_url}{self.path}" , headers=self.headers, timeout= 30 ) async def patch (self, json: Mapping[ str , Any]) -> httpx.Response: """PATCHãªã¯ãšã¹ããéä¿¡ããŸã""" async with httpx.AsyncClient() as client: return await client.patch(f "{self.base_url}{self.path}" , headers=self.headers, json=json, timeout= 30 ) async def post (self, json: Mapping[ str , Any]) -> httpx.Response: """POSTãªã¯ãšã¹ããéä¿¡ããŸã""" async with httpx.AsyncClient() as client: return await client.post(f "{self.base_url}{self.path}" , headers=self.headers, json=json, timeout= 30 ) 5. åè«ïŒOpportunityïŒã®äœæ å¿
èŠãªããŒã¿ åè«åãã¹ããŒãžãååŒå
æ
å ±ãªã©ãå¿
èŠã«å¿ããŠè¿œå ããŠãã ãã APIãªã¯ãšã¹ãã®æ§ç¯ POST ã¡ãœãããçšããŠãæå®ã®ãšã³ããã€ã³ããžJSON圢åŒã§ããŒã¿ãéä¿¡ããŸããSalesforce API㯠Content-Length ããããå¿
èŠãšãªãå Žåããããããäºåã«JSONæååã®é·ããèšç®ããŠèšå®ããŸã ãšã³ããã€ã³ãäŸ path="/services/data/vXX.X/sobjects/Opportunity" ããããŒäŸ headers={"Content-Length": str(len(json.dumps(data)))} ãµã³ãã«ã³ãŒã 以äžã¯Pythonã«ããå®è£
äŸã§ããéåæHTTPã¯ã©ã€ã¢ã³ãïŒhttpxïŒãçšããŠSalesforce APIã«POSTãªã¯ãšã¹ããéä¿¡ããåè«ãäœæããŸã import asyncio class SalesforceOpportunity : async def create_opportunity (self, data: Mapping[ str , Any]) -> httpx.Response: """Salesforceã®åè«ãAPIã§äœæããŸã Args: data (Mapping[str, Any]): äœæããåè«ã®æ
å ±ãå«ãã èŸæžåããŒã¿ Returns: httpx.Response: Salesforce APIããã®ã¬ã¹ãã³ã¹ """ # APIã¯ã©ã€ã¢ã³ããåæåïŒå¿
èŠãªããããèšå®ïŒ sf_api_client = SalesforceRestApiClient( path= "/services/data/vXX.X/sobjects/Opportunity" , additional_headers={ "Content-Length" : str ( len (json.dumps(data))), }, ) return await sf_api_client.post(json=data) if __name__ == "__main__" : # å®è¡äŸïŒåè«ãäœæããŸã async def main (): sf = SalesforceOpportunity() response = await sf.create_opportunity( { "Name" : "Test Opportunity" , "StageName" : "Prospecting" , "CloseDate" : "2024-12-31" , "AccountId" : "0015g00000A3X7dAAF" , # æå¹ãªAccountIdãèšå®ããŠãã ãã } ) print (f "{response.status_code=}" ) print (f "{response.json()=}" ) asyncio.run(main()) ãšã©ãŒãã³ããªã³ã° Salesforce APIãžã®POSTæã«ã¯ã 201 Created ãæåæã®å
žåçãªã¹ããŒã¿ã¹ã³ãŒãã§ãããšã©ãŒæã«ã¯ 400 ã 404 ãªã©ãè¿ããã¬ã¹ãã³ã¹ããã£å
ã« errorCode ã fields ãªã©ã®è©³çްãå«ãŸããŸã 以äžã¯ãšã©ãŒãçºçããå Žåã®äŸã§ã [ { "message": "äžæ£ãªçš®å¥ã® ID å€: 0015g00000A3X7dAAF", "errorCode": "MALFORMED_ID", "fields": ["AccountId"] } ] ãã®ãããªãšã©ãŒã«å¯ŸããŠã¯ããã°åºåããªãã©ã€ãé©åãªãšã©ãŒã¡ãã»ãŒãžã®ãŠãŒã¶ãŒéç¥ãªã©ãè¡ããŸãã 6. èŠç©ïŒQuoteïŒã®äœæ åè«ãšã®é¢é£ä»ã èŠç©ãšåè«ã¯ã SBQQ__Opportunity2__c ãã£ãŒã«ãã§é¢é£ä»ããŸã å¿
èŠãªãã£ãŒã«ã åè«IDãäŸ¡æ Œè¡šIDãæéæ¥ãªã©ãèŠä»¶ã«å¿ããŠè¿œå ãã£ãŒã«ããã«ã¹ã¿ã ãã£ãŒã«ããèšå®ããŸã APIãªã¯ãšã¹ãã®è©³çް POST ãªã¯ãšã¹ãã䜿çšããŠã SBQQ__Quote__c ãªããžã§ã¯ãã«ããŒã¿ãéä¿¡ããŸã ãšã³ããã€ã³ãäŸ path="/services/data/vXX.X/sobjects/SBQQ__Quote__c" ããããŒäŸ headers={"Content-Length": str(len(json.dumps(data)))} ãµã³ãã«ã³ãŒã 以äžã¯ãPythonã䜿çšããŠèŠç©ãäœæãããµã³ãã«ã³ãŒãã§ã import asyncio class SalesforceQuote : async def create_quote (self, data: Mapping[ str , Any]) -> httpx.Response: """Salesforceã®èŠç©ãAPIã§äœæããŸã Args: data (Mapping[str, Any]): äœæããèŠç©ã®æ
å ±ãå«ãã èŸæžåããŒã¿ Returns: httpx.Response: Salesforce APIããã®ã¬ã¹ãã³ã¹ """ # APIã¯ã©ã€ã¢ã³ããåæåïŒå¿
èŠãªããããèšå®ïŒ sf_api_client = SalesforceRestApiClient( path= "/services/data/vXX.X/sobjects/SBQQ__Quote__c" , additional_headers={ "Content-Length" : str ( len (json.dumps(data))), }, ) return await sf_api_client.post(json=data) if __name__ == "__main__" : # å®è¡äŸïŒèŠç©ãäœæããŸã async def main (): sf = SalesforceQuote() # 以äžã¯äŸãšããŠã®ãã£ãŒã«ãèšå®ã§ããå®éã®IDãå€ã¯æå¹ãªãã®ãæå®ããŠãã ããã response = await sf.create_quote( { "SBQQ__BillingCity__c" : "Chiyoda" , "SBQQ__BillingPostalCode__c" : "100-0000" , "SBQQ__BillingState__c" : "Tokyo" , "SBQQ__BillingStreet__c" : "1-1-1" , "SBQQ__EndDate__c" : "2024-12-31" , "SBQQ__Opportunity2__c" : "0065g00000B3X7dAAF" , # åè«ID "SBQQ__PricebookId__c" : "01s5g00000A3X7dAAF" , # äŸ¡æ Œè¡šID "SBQQ__PrimaryContact__c" : None , "SBQQ__Primary__c" : True , "SBQQ__QuoteTemplateId__c" : "a1s5g0000003X7dAAF" , # ãã³ãã¬ãŒãID "SBQQ__StartDate__c" : "2024-01-01" , "SBQQ__SubscriptionTerm__c" : 12 , } ) print (f "{response.status_code=}" ) print (f "{response.json()=}" ) asyncio.run(main()) ãšã©ãŒãã³ããªã³ã° ãšã©ãŒãçºçããå Žåã 400 Bad Request ãªã©ã®ã¹ããŒã¿ã¹ã³ãŒããšãšãã«ã errorCode ã message ãè¿ãããŸãã以äžã¯äžè¬çãªãšã©ãŒå¿çäŸã§ã [ { "message": "invalid cross reference id", "errorCode": "INVALID_CROSS_REFERENCE_KEY", "fields": [] } ] ãã®ãããªãšã©ãŒã«å¯ŸããŠã¯ããã°åºåãIDã®å確èªãå¿
èŠãªããŒã¿ãã£ãŒã«ãã®ä¿®æ£ãè¡ããŸã 7. èŠç©åç®ïŒQuote Line ItemsïŒã®ç»é² æé èŠç©ã®èªã¿åã ååã®èªã¿åã ååã®è¿œå èŠç©ã®èšç® èŠç©ã®ä¿å ãããã®ã¹ããããéããŠãèŠç©åç®ãèªåçã«è¿œå ã§ããŸã 補åæ
å ±ã®æºå 補åIDãæ°éãäŸ¡æ Œãªã©ã®ããŒã¿ãå¿
èŠã«å¿ããŠè¿œå ãã£ãŒã«ããèšå®ã§ããŸã APIãªã¯ãšã¹ãã®æ§ç¯ èŠç©IDãåºã«èŠç©åç®ã远å ããã«ã¯ãCPQåºæã®ãšã³ããã€ã³ãã䜿çšããŸãã QuoteReader ã ProductLoader ã QuoteProductAdder ã QuoteCalculator ã QuoteSaver ãšãã£ãCPQ APIãšã³ããã€ã³ããé çªã«åŒã³åºãããšã§ãäžé£ã®åŠçãèªååã§ããŸã ãã«ã¯æäœã®èæ
® è€æ°ã®èŠç©åç®ãäžåºŠã«ç»é²ããå Žåãäžæ¬åŠççšã®ã³ã³ããã¹ãããŸãšããŠéä¿¡ããããšã§ãããã©ãŒãã³ã¹ãæé©åã§ããŸã ååãäžæ¬è¿œå ããåŸãèŠç©ãåèšç®ããæåŸã«ä¿åããæµãã§åŠçãå®çµãããŸã ãµã³ãã«ã³ãŒã 以äžã®ãµã³ãã«ã³ãŒãã¯ãèŠç©ã«ååã远å ããäžé£ã®æµããCPQ APIã§å®çŸããŸã import asyncio class SalesforceCpqQuote : async def get_quote (self, quote_id: str ) -> httpx.Response: """Salesforceã®èŠç©ãCPQ APIã§ååŸããŸã""" sf_api_client = SalesforceRestApiClient( path=f "/services/apexrest/SBQQ/ServiceRouter?reader=SBQQ.QuoteAPI.QuoteReader&uid={quote_id}" , ) return await sf_api_client.get() async def get_product (self, product_id: str , data: Mapping[ str , str ]) -> httpx.Response: """Salesforceã®ååãCPQ APIã§ååŸããŸã""" sf_api_client = SalesforceRestApiClient( path=f "/services/apexrest/SBQQ/ServiceRouter?loader=SBQQ.ProductAPI.ProductLoader&uid={product_id}" , additional_headers={ "Content-Length" : str ( len (json.dumps(data))), }, ) return await sf_api_client.patch(json=data) async def add_product (self, data: Mapping[ str , str ]) -> httpx.Response: """Salesforceã®èŠç©åç®ãCPQ APIã§äœæããŸã""" sf_api_client = SalesforceRestApiClient( path= "/services/apexrest/SBQQ/ServiceRouter?loader=SBQQ.QuoteAPI.QuoteProductAdder" , additional_headers={ "Content-Length" : str ( len (json.dumps(data))), }, ) return await sf_api_client.patch(json=data) async def calculate_quote (self, data: Mapping[ str , str ]) -> httpx.Response: """Salesforceã®èŠç©ãCPQ APIã§èšç®ããŸã""" sf_api_client = SalesforceRestApiClient( path= "/services/apexrest/SBQQ/ServiceRouter?loader=SBQQ.QuoteAPI.QuoteCalculator" , additional_headers={ "Content-Length" : str ( len (json.dumps(data))), }, ) return await sf_api_client.patch(json=data) async def save_quote (self, data: Mapping[ str , str ]) -> httpx.Response: """Salesforceã®èŠç©ãCPQ APIã§ä¿åããŸã""" sf_api_client = SalesforceRestApiClient( path= "/services/apexrest/SBQQ/ServiceRouter" , additional_headers={ "Content-Length" : str ( len (json.dumps(data))), }, ) return await sf_api_client.post(json=data) class CreateQuoteLineItem : def __init__ (self) -> None : self.salesforce_cpq_quote = SalesforceCpqQuote() async def execute ( self, quote_id: str , product_id: str , pricebook_id: str , currency_code: str , product_counts: dict , ): """åŠçã®æµã: 1. èŠç©ã®èªã¿åã 2. ååã®èªã¿èŸŒã¿ 3. ååã®è¿œå 4. èŠç©ã®èšç® 5. èŠç©ã®ä¿å """ # èŠç©ã®èªã¿åã cpq_quote = await self.get_quote(quote_id) print (f "Successfully get quote: {cpq_quote}" ) # ååã®èªã¿èŸŒã¿ product = await self.get_product(product_id, pricebook_id, currency_code) print (f "Successfully get product: {product}" ) # ååã®è¿œå add_product_to_quote = await self.add_product_to_quote(product_counts, cpq_quote, product) print (f "Successfully add product: {add_product_to_quote}" ) # èŠç©ã®èšç® calculate_quote = await self.calculate_quote(add_product_to_quote) print (f "Successfully calculate quote: {calculate_quote}" ) # èŠç©ã®ä¿å save_quote = await self.save_quote(calculate_quote) print (f "Successfully save quote: {save_quote}" ) async def get_quote (self, quote_id: str ) -> dict : """Salesforceã®èŠç©ãååŸ""" quote_response = await self.salesforce_cpq_quote.get_quote(quote_id) return json.loads(quote_response.json()) async def get_product (self, product_id: str , pricebook_id: str , currency_code: str ) -> dict : """Salesforceã®ååãååŸ""" product_data = { "context" : json.dumps(ProductGetContext(pricebookId=pricebook_id, currencyCode=currency_code).dict()) } product_response = await self.salesforce_cpq_quote.get_product(product_id, product_data) return json.loads(product_response.json()) async def add_product_to_quote (self, product_counts: dict , quote: dict , product: dict ) -> dict : """Salesforceã®ååãèŠç©ã«è¿œå """ # ProductModelãConfigurationModelãªã© product_model = ProductModel(**product) list_of_product_model = [] list_of_configuration_model = [] # ãã³ãã«ååã®åååã远å for mb_op in product_model.options: product_id = mb_op.record[ "SBQQ__OptionalSKU__c" ] quantity = product_counts.get(product_id) if quantity: mb_op.record[ "SBQQ__Quantity__c" ] = quantity cf_model = ConfigurationModel( configuredProductId=product_id, optionId=mb_op.record[ "Id" ], optionData=mb_op.record, configurationData=mb_op.record, inheritedConfigurationData= None , optionConfigurations=[], configured= False , changedByProductActions= False , isDynamicOption= False , isUpgrade= False , disabledOptionIds=[], hiddenOptionIds=[], listPrice= None , priceEditable= False , validationMessages=[], dynamicOptionKey= None , ) list_of_configuration_model.append(cf_model.dict()) # ãã³ãã«å忬äœãžã®ãªãã·ã§ã³è¿œå if product_model.configuration is not None : if product_model.configuration.optionConfigurations is not None : product_model.configuration.optionConfigurations.extend(list_of_configuration_model) product_model.configuration.configured = True list_of_product_model.append(product_model.dict()) # èŠç©ãšååã¢ãã«ãcontextã«ã»ãã context = ProductAddContext( quote={k: v for k, v in quote.items() if k != "ui_original_record" }, products=list_of_product_model, ) add_product_response = await self.salesforce_cpq_quote.add_product({ "context" : json.dumps(context.dict())}) return json.loads(add_product_response.json()) async def calculate_quote (self, quote: dict ) -> dict : """Salesforceã®èŠç©ãèšç®""" calculate_quote_data = { "context" : json.dumps({ "quote" : {k: v for k, v in quote.items() if k != "ui_original_record" }}) } calculate_quote_response = await self.salesforce_cpq_quote.calculate_quote(calculate_quote_data) return json.loads(calculate_quote_response.json()) async def save_quote (self, quote: dict ) -> dict : """Salesforceã®èŠç©ãä¿å""" save_quote_data = { "saver" : "SBQQ.QuoteAPI.QuoteSaver" , "model" : json.dumps({k: v for k, v in quote.items() if k != "ui_original_record" }), } save_quote_response = await self.salesforce_cpq_quote.save_quote(save_quote_data) return json.loads(save_quote_response.json()) if __name__ == "__main__" : """èŠç©åç®ã®è¿œå ãå®è¡ããäŸã§ããå®éã«ã¯æå¹ãªIDãšé貚ã³ãŒããèšå®ããŠãã ãã""" async def main (): quote_id = "a0B5g00000DwJtEEAV" # èŠç©ID product_id = "01t5g00000B1Q0PAK" # ååãã³ãã«ID pricebook_id = "01s5g0000008Q5eAAE" # äŸ¡æ Œè¡šID currency_code = "JPY" # é貚ã³ãŒã product_counts = { "01t5g00000B1Q0MKA" : 1 , # èŠç©ååIDãšæ°é "01t5g00000B1Q0NAAV" : 2 , } await CreateQuoteLineItem().execute( quote_id, product_id, pricebook_id, currency_code, product_counts, ) asyncio.run(main()) CPQ API ã®ãªã¯ãšã¹ãã¢ãã«å®çŸ© 以äžã¯ãCPQ APIãšã®ãããšãã§äœ¿çšããããŒã¿ã¢ãã«ã®äŸã§ãã pydantic ãçšããŠã¹ããŒããå®çŸ©ããããªããŒã·ã§ã³ãã³ã¡ã³ããæç¢ºã«ããŠããŸãããããã®ã¢ãã«ã¯ãåãåã£ãJSONããŒã¿ãæç¢ºãªåæ
å ±ã®ããPythonãªããžã§ã¯ããšããŠæ±ãããšã§ãå¯èªæ§ãåäžãããŸã from pydantic import BaseModel, Field class ConfigurationModel (BaseModel): configuredProductId: str = Field(..., title= "ååID" , description= "The Product2.Id" , example= "01t6F00000B8XZTAA3" ) optionId: str | None = Field( default= None , title= "ãªãã·ã§ã³ID" , description= "The SBQQ__ProductOption__c.Id" , example= "01t6F00000B8XZTAA3" ) optionData: dict = Field( ..., title= "ãªãã·ã§ã³ããŒã¿" , description= "Editable data about the option, such as quantity or discount" , example={ "Id" : "01t6F00000B8XZTAA3" }, ) configurationData: dict = Field( ..., title= "æ§æããŒã¿" , description= "Stores the values of the configuration attributes." , example={ "Id" : "01t6F00000B8XZTAA3" }, ) inheritedConfigurationData: dict | None = Field( default= None , title= "ç¶æ¿ãããæ§æããŒã¿" , description= "Stores the values of the inherited configuration attributes." , example={ "Id" : "01t6F00000B8XZTAA3" }, ) optionConfigurations: list = Field( ..., title= "ãªãã·ã§ã³æ§æ" , description= "Stores the options selected on this product." , example=[{ "Id" : "01t6F00000B8XZTAA3" }], ) configured: bool = Field( ..., title= "æ§ææžã¿" , description= "Indicates whether the product has been configured." , example= False ) changedByProductActions: bool = Field( ..., title= "ååã¢ã¯ã·ã§ã³ã«ãã倿Ž" , description= "Indicates whether a product action changed the configuration of this bundle." , example= False , ) isDynamicOption: bool = Field( ..., title= "åçãªãã·ã§ã³" , description= "Indicates whether the product was configured using a dynamic lookup." , example= False , ) isUpgrade: bool = Field( ..., title= "ã¢ããã°ã¬ãŒã" , description= "Queries whether this product is an upgrade." , example= False ) disabledOptionIds: list = Field( default= None , title= "ç¡å¹ãªãªãã·ã§ã³ID" , description= "The option IDs that are disabled." , example=[ "01t6F00000B8XZTAA3" ], ) hiddenOptionIds: list = Field( default= None , title= "é衚瀺ãªãã·ã§ã³ID" , description= "The option IDs that are hidden." , example=[ "01t6F00000B8XZTAA3" ], ) listPrice: float | None = Field(default= None , title= "å®äŸ¡" , description= "The list price." , example= 0.0 ) priceEditable: bool = Field( ..., title= "äŸ¡æ Œç·šéå¯èœ" , description= "Indicates whether the price is editable." , example= False ) validationMessages: list = Field( ..., title= "æ€èšŒã¡ãã»ãŒãž" , description= "Validation messages." , example=[ "Error message" ] ) dynamicOptionKey: str | None = Field( default= None , title= "åçãªãã·ã§ã³ããŒ" , description= "Internal property for dynamic options." , example= "01t6F00000B8XZTAA3" , ) class OptionModel (BaseModel): record: dict = Field( ..., title= "ãªãã·ã§ã³" , description= "The record that this model represents." , example={ "Id" : "01t6F00000B8XZTAA3" }, ) externalConfigurationData: dict | None = Field( default= None , title= "å€éšæ§æããŒã¿" , description= "Internal property for the external configurator feature." , example={ "Id" : "01t6F00000B8XZTAA3" }, ) configurable: bool = Field( ..., title= "æ§æå¯èœ" , description= "Indicates whether the option is configurable." , example= False ) configurationRequired: bool = Field( ..., title= "æ§æå¿
é " , description= "Indicates whether the configuration of the option is required." , example= False , ) quantityEditable: bool = Field( ..., title= "æ°éç·šéå¯èœ" , description= "Indicates whether the quantity is editable." , example= False ) priceEditable: bool = Field( ..., title= "äŸ¡æ Œç·šéå¯èœ" , description= "Indicates whether the price is editable." , example= False ) productQuantityScale: float | None = Field( default= None , title= "ååæ°éã¹ã±ãŒã«" , description= "Returns the value of the quantity scale field for the product being configured." , example= 0.0 , ) priorOptionExists: bool | None = Field( default= None , title= "åã®ãªãã·ã§ã³ãååšãã" , description= "Checks if this option is an asset on the account that the quote is associated with." , example= False , ) dependentIds: list = Field( ..., title= "äŸåãããªãã·ã§ã³ID" , description= "The option IDs that depend on this option." , example=[ "01t6F00000B8XZTAA3" ], ) controllingGroups: dict = Field( ..., title= "å¶åŸ¡ã°ã«ãŒã" , description= "The option IDs that this option depends on." , example={ "Id" : "01t6F00000B8XZTAA3" }, ) exclusionGroups: dict = Field( ..., title= "æä»ã°ã«ãŒã" , description= "The option IDs that this option is exclusive with." , example={ "Id" : "01t6F00000B8XZTAA3" }, ) reconfigureDimensionWarning: str = Field( ..., title= "åæ§ææ¬¡å
èŠå" , description= "Reconfigures the warning label for an option with segments." , example= "01t6F00000B8XZTAA3" , ) hasDimension: bool = Field( ..., title= "次å
ããã" , description= "Indicates whether this option has dimensions or segments." , example= False ) isUpgrade: bool = Field( ..., title= "ã¢ããã°ã¬ãŒã" , description= "Indicates whether the product option is related to an upgrade product." , example= False , ) dynamicOptionKey: str | None = Field( default= None , title= "åçãªãã·ã§ã³ããŒ" , description= "Internal property for dynamic options." , example= "01t6F00000B8XZTAA3" , ) class FeatureModel (BaseModel): record: dict = Field( ..., title= "æ©èœ" , description= "The record that this model represents." , example={ "Id" : "01t6F00000B8XZTAA3" } ) instructionsText: str | None = Field( default= None , title= "æç€ºããã¹ã" , description= "Instruction label for the feature." , example= "01t6F00000B8XZTAA3" , ) containsUpgrades: bool = Field( ..., title= "ã¢ããã°ã¬ãŒããå«ãŸããŠãã" , description= "This feature is related to an upgrade product." , example= False , ) class ConfigAttributeModel (BaseModel): name: str | None = Field( default= None , title= "åå" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.Name." , example= "01t6F00000B8XZTAA3" , ) targetFieldName: str = Field( ..., title= "ã¿ãŒã²ãããã£ãŒã«ãå" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__TargetField__c." , example= "01t6F00000B8XZTAA3" , ) displayOrder: float | None = Field( default= None , title= "衚瀺é " , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__DisplayOrder__c." , example= 0.0 , ) columnOrder: str = Field( ..., title= "ã«ã©ã é " , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ColumnOrder__c." , example= "01t6F00000B8XZTAA3" , ) required: bool = Field( ..., title= "å¿
é " , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Required__c." , example= False , ) featureId: str = Field( ..., title= "æ©èœID" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Feature__c." , example= "01t6F00000B8XZTAA3" , ) position: str = Field( ..., title= "äœçœ®" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Position__c." , example= "01t6F00000B8XZTAA3" , ) appliedImmediately: bool = Field( ..., title= "çŽã¡ã«é©çš" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__AppliedImmediately__c." , example= False , ) applyToProductOptions: bool = Field( ..., title= "ååãªãã·ã§ã³ã«é©çš" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ApplyToProductOptions__c." , example= False , ) autoSelect: bool = Field( ..., title= "èªåéžæ" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__AutoSelect__c." , example= False , ) shownValues: list | None = Field( default= None , title= "衚瀺å€" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__ShownValues__c." , example=[ "01t6F00000B8XZTAA3" ], ) hiddenValues: list | None = Field( default= None , title= "é衚瀺å€" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__HiddenValues__c." , example=[ "01t6F00000B8XZTAA3" ], ) hidden: bool = Field( ..., title= "é衚瀺" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.SBQQ__Hidden__c." , example= False , ) noSuchFieldName: str | None = Field( default= None , title= "ååšããªããã£ãŒã«ãå" , description= "If no field with the target name exists, the target name is stored here." , example= "01t6F00000B8XZTAA3" , ) myId: str = Field( ..., title= "ID" , description= "Corresponds directly to SBQQ__ConfigurationAttribute__c.Id." , example= "01t6F00000B8XZTAA3" , ) class ConstraintModel (BaseModel): record: dict = Field( ..., title= "å¶çŽ" , description= "The record that this model represents." , example={ "Id" : "01t6F00000B8XZTAA3" } ) priorOptionExists: bool = Field( ..., title= "åã®ãªãã·ã§ã³ãååšãã" , description= "Checks if this option is an asset on the account that the quote is associated with." , example= False , ) class ProductModel (BaseModel): record: dict = Field( ..., title= "åå" , description= "The record that this model represents." , example={ "Id" : "01t6F00000B8XZTAA3" } ) upgradedAssetId: str | None = Field( default= None , title= "SBQQ__QuoteLine__c.SBQQ__UpgradedAsset__c.Id" , description= "Provides a source for SBQQ__QuoteLine__c.SBQQ__UpgradedAsset__c." , example= "01t6F00000B8XZTAA3" , ) currencySymbol: str = Field( ..., title= "é貚ã·ã³ãã«" , description= "The symbol for the currency in use." , example= "Â¥" ) currencyCode: str = Field( ..., title= "é貚ã³ãŒã" , description= "The ISO code for the currency in use." , example= "JPY" ) featureCategories: list = Field( ..., title= "æ©èœã«ããŽãª" , description= "Allows users to sort product features by category." , example=[ "01t6F00000B8XZTAA3" ], ) options: list [OptionModel] = Field( ..., title= "ãªãã·ã§ã³" , description= "A list of all available options for this product." , example=[ "01t6F00000B8XZTAA3" ], ) features: list [FeatureModel] = Field( ..., title= "æ©èœ" , description= "All features available for this product" , example=[ "01t6F00000B8XZTAA3" ] ) configuration: ConfigurationModel = Field( ..., title= "æ§æ" , description= "An object representing this productâs current configuration." , example={ "Id" : "01t6F00000B8XZTAA3" }, ) configurationAttributes: list [ConfigAttributeModel] = Field( ..., title= "æ§æå±æ§" , description= "All configuration attributes available for this product." , example=[ "01t6F00000B8XZTAA3" ], ) inheritedConfigurationAttributes: list [ConfigAttributeModel] | None = Field( default= None , title= "ç¶æ¿ãããæ§æå±æ§" , description= "All configuration attributes that this product inherits from ancestor products." , example=[ "01t6F00000B8XZTAA3" ], ) constraints: list [ConstraintModel] = Field( ..., title= "å¶çŽ" , description= "Option constraints on this product." , example=[ "01t6F00000B8XZTAA3" ] ) class ProductGetContext (BaseModel): pricebookId: str = Field( ..., title= "äŸ¡æ Œè¡šID" , description= "The ID of the price book to use." , example= "01s6F00000CneeJQAR" ) currencyCode: str = Field( ..., title= "é貚ã³ãŒã" , description= "The ISO code for the currency in use." , example= "JPY" ) class ProductAddContext (BaseModel): ignoreCalculate: bool = Field(default= True , title= "èšç®ç¡èŠ" , description= "èšç®ç¡èŠ" , example= True ) quote: dict = Field(..., title= "èŠç©" , description= "èŠç©ã¢ãã«" , example={}) products: list = Field(..., title= "ååãªã¹ã" , description= "ååã¢ãã«" , example=[]) groupKey: int = Field(default= 0 , title= "ã°ã«ãŒãããŒ" , description= "ã°ã«ãŒãããŒ" , example= 0 ) ãšã©ãŒãã³ããªã³ã° ãšã©ãŒãçºçããå Žåã¯HTTPã¹ããŒã¿ã¹ã³ãŒããšãšã©ãŒã¡ãã»ãŒãžãè¿ãããŸããããšãã°ã 500 Internal Server Error ãªã©ãè¿ãããå Žåãã¬ã¹ãã³ã¹æ¬æã«ã¯ errorCode ã message ãã£ãŒã«ããå«ãŸããŸãã [ { "errorCode": "APEX_ERROR", "message": "System.AssertException: Assertion Failed: Unsupported quote object: a0B5g00000DwJtEEAV\n\n(System Code)", } ] 8. ãã€ã³ãã®æ¯ãè¿ã 1. åè«ïŒOpportunityïŒã®äœæ å¿
èŠãªããŒã¿ïŒåè«åãã¹ããŒãžãååŒå
IDãCloseDateãªã©ïŒãæºåããŸã POST /services/data/vXX.X/sobjects/Opportunity ãšã³ããã€ã³ããçšããJSON圢åŒã§ããŒã¿ãéä¿¡ããŸã 2. èŠç©ïŒQuoteïŒã®äœæ åè«IDãäŸ¡æ Œè¡šIDãæéãéå§æ¥ãªã©ã®å¿
é é
ç®ãæå®ããŸã POST /services/data/vXX.X/sobjects/SBQQ__Quote__c ã䜿çšããJSON圢åŒã§ããŒã¿ãéä¿¡ããŸã 3. èŠç©åç®ïŒQuote Line ItemsïŒã®ç»é² CPQ APIåºæã®ãããŒïŒ QuoteReader , ProductLoader , QuoteProductAdder , QuoteCalculator , QuoteSaver ïŒãé åºç«ãŠãŠå®è¡ããŸã 補åIDãæ°éããªãã·ã§ã³æ§æãªã©ãäºåã«çšæãããã³ãã«æ§æååã«å¯Ÿå¿ããŸã è€æ°ååã®äžæ¬è¿œå æã¯ããªã¯ãšã¹ãããŸãšããŠéä¿¡ããããã©ãŒãã³ã¹ãæé©åããŸã 9. èªååã«ããåŸããã广 èªååã«ããã以äžã®ãããªå¹æãåŸãããŸããã æåäœæ¥ãæžå°ãããã¥ãŒãã³ãšã©ãŒãæå¶ãããŸãã å¶æ¥æ
åœè
ãã³ã¢æ¥åã«éäžã§ããç°å¢ãæŽããæ¥åå¹çãåäžããŸãã èŠç©äœæããæ¿èªãŸã§ã®ãªãŒãã¿ã€ã ãççž®ããã顧客察å¿ã¹ããŒããšæºè¶³åºŠãåäžããŸãã 10. èŠåŽããç¹ã»ãããã©ãã éçºéçšã§ã¯ã以äžã®ãããªèª²é¡ã«çŽé¢ããŸããã Salesforce CPQ APIã«é¢ããããã¥ã¡ã³ããäºäŸãå°ãªããé©åãªãšã³ããã€ã³ãéžå®ãããŒã¿ã¢ãã«çè§£ãŸã§ã«è©Šè¡é¯èª€ãå¿
èŠã§ãã é¢é£IDãäŸåé¢ä¿ã®æ£ç¢ºãªææ¡ãé£ããããšã©ãŒã®è§£æ¶ã«æéãããããŸãã é©åãªãšã©ãŒãã³ããªã³ã°ãPydanticã§ã®ããŒã¿ã¢ãã«å®çŸ©ãªã©ãPythonå®è£
äžã®ãã¹ããã©ã¯ãã£ã¹ãæ¢ããªããéçºãé²ããå¿
èŠããããŸãã 11. ãŸãšã æ¬èšäºã§ã¯ãSalesforce CPQ APIãçšããŠãåè«ããèŠç©äœæãèŠç©åç®ã®ç»é²ãŸã§ãèªååããå
·äœçãªæé ãšãã€ã³ããã玹ä»ããŸãããããã«ãããæäœæ¥ãæžãããŠãã¥ãŒãã³ãšã©ãŒãæããæ¥åã¹ããŒããåäžãããããšã§ã顧客æºè¶³åºŠãé«ããããå¯èœæ§ãèŠããŠããããšæããŸãã ãã®èšäºãå°ãã§ãåèã«ãªããèªè
ã®æ¹ã
ã®éçºãæ¥åæ¹åã«ã圹ç«ãŠããã ããã°å¹žãã§ãã 12. åèæç® Salesforceå
¬åŒããã¥ã¡ã³ã Salesforce CPQ APIã¬ã€ã åèäŸã³ãŒãïŒGitHub GistïŒ https://gist.github.com/paustint/40b602503b6cd6ae879af7b85d910da8