Skip to the lesson
CivOps AI Academy · M15Customers and CRM: Accounts, Pipeline and Quotes on One Spine
0%

Chapter 1 · Accounts, contacts and leads

One customer, one record

A customer relationship tool keeps who your customers are and where each sale stands. This chapter shows what it replaces, why a company is stored once for the whole business, and how a lead becomes an account, a contact and a deal without losing where it came from.

25 minAccounts on core_party4 record shapesLead row kept

By the end of this chapter you can

  • Say what the CRM module covers, what it leaves to other modules and which subscriptions it replaces.
  • Explain why an account points at the shared party table instead of holding its own company record.
  • Describe what lead conversion creates and what it must never change.
  • Name the consent and do-not-contact fields and the written rule that merges duplicates.

What the module replaces

The module manages the revenue pipeline from a first enquiry to a won order: accounts and contacts, leads and how they are qualified, deals that move through stages, the calls, emails and meetings around them, and quotes priced from a price book or estimated from a routing and a bill of materials. It replaces the customer relationship management tools a business usually pays for: Salesforce Sales Cloud, HubSpot Sales Hub, Microsoft Dynamics 365 Sales, Pipedrive, Zoho CRM and Close, and the quoting tools Salesforce Revenue Cloud, DealHub and Paperless Parts. HubSpot describes its data as a small set of linked objects such as contacts, companies and deals [1]. This module keeps the same objects, but on the platform's own spine, so a customer is one record that sales, service and billing share.

Inside this moduleLeft to another moduleOwner
Accounts, contacts, leads, conversion and duplicate mergingMarketing campaigns, scoring and nurtureM16
Pipelines, stages, stage history, contact roles, forecastingSupport ticketsM19
Activities, price books, quotes, approvals, acceptanceOrder fulfilment and invoicingM12 and accounting
Routing and bill-of-materials cost estimates for made-to-order quotesBuilding the routing and the bill of materialsM04 and M14

The specification lists fifteen crm_ tables. This course builds thirteen of them: crm_account, crm_contact, crm_lead, crm_pipeline, crm_pipeline_stage, crm_opportunity, crm_stage_history, crm_contact_role, crm_activity, crm_price_book, crm_price_book_entry, crm_quote and crm_quote_line. Chapter 2 lists every column of the thirteen. The other two, crm_cost_estimate and crm_territory, are explained in chapters 2 and 3 so you can add them later.

A company is stored once

Every business has one list of companies and people. The Foundation spine already holds it as core_party (a company or organisation) and core_person (a human). A CRM account is not a second company record. It is a crm_account row whose party_id points at the one core_party row, one to one, and adds only what sales needs: the account_type (prospect, customer, partner, competitor or distributor), industry, segment, owner, employee and revenue bands, a parent account for company groups, and a lifecycle_stage (target, engaged, opportunity, customer or churned).

The party modelA core_party row is the company once. A crm_account points at it one to one and can point at a parent account. A crm_contact belongs to an account and points at a core_person. A separate company table is shown as the pitfall to avoid. A company is stored once, in the shared party table. Sales adds a view of it.core_partythe company, oncecrm_accountthe sales view of itcrm_contacta person at the accountparty_idone to oneaccount_idparent_account_idcore_persona human, onceperson_idA separate company tableduplicates core_partyA second company table is the pitfall: two records for one customer drift apart.
One company, one record. The account and the contact are views of rows in the shared party and person tables. A separate company table is the common pitfall: it drifts away from the real record the moment one of them is edited.

A contact is a person at an account: a crm_contact row with its own first and last name, title, email, phone, owner and a lifecycle_stage that runs from subscriber, lead, mql (a marketing qualified lead, someone marketing judges ready for sales) and sql (a sales qualified lead, someone sales has accepted as worth pursuing) through opportunity and customer to evangelist or other. It also points at a core_person of type customer_contact. The shapes (account, contact, lead, opportunity, quote) follow the reference entities in the Microsoft Common Data Model [2], and the public vocabularies schema.org Organization [3] and schema.org Person [4] are what you use when you hand records to another system.

ShapeWhat it isTableStatus or stage values
AccountA company you sell to, or compete withcrm_accountlifecycle_stage: target, engaged, opportunity, customer, churned
ContactA person at an accountcrm_contactlifecycle_stage: subscriber, lead, mql, sql, opportunity, customer, evangelist, other
LeadAn unqualified prospect, kept as it first arrivedcrm_leadstatus: new, working, nurturing, qualified, unqualified, converted
OpportunityA deal that can be won or lostcrm_opportunitya stage from the pipeline

Leads and conversion

A lead comes from a web form or an import with a name, a company, an email, a phone, a source, an owner and the campaign tags in the page address (utm_source, utm_medium and so on) stored as utm. The rep records what was learned while qualifying as a JSON column named qualification. The specification names BANT (budget, authority, need, timing) and MEDDICC (metrics, economic buyer, decision criteria, decision process, identify the pain, champion, competition) as examples; use the fields your business actually asks.

Conversion is one database function that runs as a single transaction (all of its changes happen, or none do). It reuses the core_party if the company already exists or makes it; it reuses the crm_account if that party already has one (an existing customer stays a customer, and a target or engaged account becomes an opportunity account), and otherwise makes it, because the database allows one account per party; it makes a core_person and the contact, makes the opportunity at the first stage with an owner and a next step, marks the lead converted with the three new ids and the time, and writes a crm.lead.converted event. The lead row itself is kept.

Converting a leadA qualified crm_lead enters crm_convert_lead, one transaction that makes or reuses an account, makes a contact, makes an opportunity and writes a crm.lead.converted event. The original lead row is kept with its status set to converted. crm_leadstatus: qualifiedcrm_convert_leadone transactioncrm_accountreused or new, via core_partycrm_contactwith its core_personcrm_opportunityfirst stage, owner, next stepcore_event_outboxcrm.lead.convertedThe lead is kept, not overwrittenstatus converted, three new ids, converted_at; utm copied
Converting a lead. Four things are created and one is kept. The lead's name, company, email, phone, source and utm never change, which is how you can later ask which campaign produced which customer.

Duplicates and consent

Two records for one customer are the most common CRM fault. Decide the rule before loading anything, write it in plain sentences and apply it the same way every time. A reasonable rule: a company is the same if its website address matches, or if its name matches after lowercasing, removing punctuation and dropping endings such as Inc, LLC and Ltd; a contact is the same if its email matches after lowercasing; a contact with no email is never merged by a script and goes to a person to review. Merging moves the dropped record's roles, activities and deals to the kept one and archives the dropped row. Nothing is deleted.

Contact data is personal data. Each contact carries do_not_contact, which is required and must be respected by every screen and route that sends a message, and the lead or contact keeps the wording of the consent shown, the time and the lawful basis relied on. The rules differ by place and by how you contact people: the United States CAN-SPAM Act for commercial email [5], the EU General Data Protection Regulation [6], and the California Consumer Privacy Act as amended by CPRA [7]. Which basis applies to your business is a decision for you and your adviser. The module records the answer you give; it does not invent one.

Exercise · Sort three real records15 minutes

You need: Your current CRM or contact list, a text editor, and your AI coding agent if you want help with the table

Use real records from the tool you pay for today. Do not paste personal details into a chat; describe each record by its type and fields.

Outcome: A one-page note with three real records sorted into the module's shapes, a written duplicate rule and the consent you hold for each person.

Knowledge check

A company you already invoice is now also a sales prospect. What does the CRM add?

Knowledge check

A qualified lead is converted. What must be true afterwards?

References

  1. HubSpot developers: understanding the CRM. https://developers.hubspot.com/docs/api/crm/understanding-the-crm
  2. Microsoft Learn: Common Data Model. https://learn.microsoft.com/en-us/common-data-model/
  3. schema.org: Organization. https://schema.org/Organization
  4. schema.org: Person. https://schema.org/Person
  5. Federal Trade Commission: CAN-SPAM Act, a compliance guide for business. https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business
  6. EUR-Lex: Regulation (EU) 2016/679, the General Data Protection Regulation. https://eur-lex.europa.eu/eli/reg/2016/679/oj
  7. California Attorney General: California Consumer Privacy Act. https://oag.ca.gov/privacy/ccpa

Chapter 2 · Opportunities, tables and screens

Pipelines, stage history and the forecast

A deal is only as useful as its history. This chapter shows how stages are defined, why every change is kept, how the pipeline is rebuilt as of any past date, and how the same rows give you a forecast and the five measures a sales manager asks for. It ends with every column of the module's thirteen tables and the screens that use them.

35 minEvery change kept13 tables, every column5 screens

By the end of this chapter you can

  • Define a pipeline and its stages with probability, won and lost flags and required fields.
  • Explain why storing only the current stage makes velocity and win-rate analysis impossible.
  • Rebuild the open pipeline as of a past date from crm_stage_history.
  • Calculate win rate, cycle time and coverage from the module's own tables.
  • Find every column, type, required flag and allowed value of the thirteen tables, and say who opens each screen.

Pipelines and stages

A crm_pipeline is a named sales process, for opportunities or for leads, and one of them is the default. A crm_pipeline_stage belongs to a pipeline and has a name, a position (seq), a probability, an is_won flag, an is_lost flag and required_fields: the columns that must be filled before a deal may enter the stage, such as amount, close_date or primary_contact_id. Setting probabilities from your own past results is better than copying a vendor's defaults, which describe someone else's business.

A crm_opportunity points at an account, a pipeline and a stage. It has a name, an amount and currency, an expected close_date, a probability that follows the stage, a forecast_category, an owner (required), a source and a next_step. When it is lost it also has a reason from the shared core_reason_code list and, if known, the competitor as a core_party. Two habits keep the pipeline honest: every open deal has an owner and a next step, and no deal is lost without a reason.

Who is on the deal

Real deals have several people. A crm_contact_role links a contact to an opportunity with one role. The seven roles are decision_maker, economic_buyer (holds the budget), champion (argues for you inside), influencer, technical_evaluator, user and blocker (opposes the deal). A deal with no economic buyer or decision maker on it is worth asking about before it is called likely.

The timeline

Calls, emails, meetings, tasks, notes, texts and site visits are crm_activity rows with a subject, a direction (inbound, outbound or internal), an owner, an outcome and a related record, so one list shows everything that happened on an account or a deal. The specification includes Gmail and Outlook sync and reminders; this course leaves sync for later, and keeps an external_id column so a later sync or an import never logs the same message twice.

Keep every stage change

The worst mistake in a home-made CRM is a stage column that is simply overwritten. It looks fine on the board and makes every serious question unanswerable: how long deals sit in each stage, where they are lost, what the pipeline looked like at the end of last quarter. The remedy is a crm_stage_history table that gets one row for every change, with the stage it came from, the stage it went to, when, who, and the amount at that moment.

One deal in stage historyA timeline in days, to scale. A deal is in Qualify for 14 days, then in Propose for 31 days, then won on day 45. Three crm_stage_history rows mark the three changes. Every stage change adds a row. Nothing is overwritten.Row 1from none to QualifyRow 2from Qualify to ProposeRow 3from Propose to WonQualifyProposeday 0day 10day 20day 30day 40day 50day 6014 days in Qualify, 31 days in Propose, 45 days from created to won.With only the current stage stored, rows 1 and 2 are gone, and so are those 14 and 31 days.
One deal, three rows. The bars are drawn to scale in days. From the rows you can read the time in each stage and the cycle time. With only the current stage stored, none of it can be recovered.

Let the database write the history, not the screen. A trigger (a rule inside the database that runs by itself when a row changes) on crm_opportunity inserts the row on every insert and every change of stage, so a change made by hand in SQL leaves a row too. A move function adds the business rules: it refuses a stage from another pipeline, refuses the move if a required field is empty and names it, and refuses an open stage with no next step.

Pipeline as of a past dateFour bars to scale. On 1 March the course seed had 3 open deals worth 21000 in Qualify and 3 worth 11000 in Propose. On 31 March it had 2 worth 18000 in Qualify and 2 worth 16000 in Propose. Open deals, grouped by stage, as of the end of each date (course seed)1 March · Qualify3 deals, 210001 March · Propose3 deals, 1100031 March · Qualify2 deals, 1800031 March · Propose2 deals, 16000Both pictures come from the same crm_stage_history rows. The bars share one scale.
The same table, two dates. In the course seed the open pipeline on 1 March is 6 deals worth 32000, and on 31 March it is 4 deals worth 34000: the count fell while the value rose. Bars share one scale.

When a deal is won, the module sets its forecast_category to closed, makes the account a customer (a partner or distributor keeps its type) and publishes crm.opportunity.won. When it is lost, its forecast_category is also set to closed and a reason is required. Both outcomes are stages with is_won or is_lost set, so they appear in the history like any other move.

Forecast and the five measures

Every deal sits in one forecast category: pipeline, best_case, commit, closed or omitted. Roll-ups add the categories by owner, by territory (a crm_territory row has a name, JSON assignment rules and an owner) and by period. Coverage compares the open pipeline to the quota.

Forecast categoriesFour forecast categories in rising confidence: pipeline, best_case, commit and closed. A fifth, omitted, is left out of the roll-ups. Totals roll up by owner, territory and period. pipelineopen, earlybest_casecould closecommitowner stands behind itclosedwonConfidence rises to the rightomittedleft out of the roll-upsRoll-ups add each category by owner, territory and period.Coverage = open pipeline in the period ÷ quota.
The forecast ladder. Confidence rises from pipeline to closed. Omitted deals are left out of the roll-ups.
KPIDefinitionWhy it matters
Win rateWon ÷ (won + lost), by count and by valueShows how often you win once a deal is decided
Sales cycle lengthMedian days from created to closed wonMedian, because a few very long deals would stretch an average
Pipeline coverageOpen pipeline in the period ÷ quotaShows whether there is enough to hit the number
Quote turnaroundHours from request to quote sentThe specification calls it key for job shops
Forecast accuracyCommit compared with actual closedShows how far to trust the next forecast

A worked example from the course seed: of six closed deals, four were won. By count the win rate is 4 ÷ 6, about 67%. By value, 13000 of 21000 was won, about 62%. The two differ because the lost deals were not the same size as the won ones. Always say which one you are quoting. The median days from first stage to won in the seed is 51.5.

Every table, column by column

Every table in the module starts with the standard columns every platform table has: id, tenant_id, created_at, created_by, updated_at, updated_by, row_version, archived_at and ext. The table below is the full definition of the thirteen tables this course builds: every column the module adds to the standard ones, with its type and whether it is required (marked req), and the allowed values where the specification lists them. Types are written as in the specification: text, int, num (a decimal number), bool, date, ts (a timestamp with time zone), uuid, json (jsonb in Postgres), money (an amount with fixed decimals, never floating point) and qty (a quantity that allows decimals); an arrow means a foreign key to that table. It is what your agent builds from in Session 2.

TableHoldsColumns: type, req = required
crm_accountThe sales view of one company, one per core_partyparty_id → core_party req, unique per tenant; account_type text req: prospect, customer, partner, competitor or distributor; industry text; segment text; owner_id → core_person; parent_account_id → crm_account; employee_band text; revenue_band text; lifecycle_stage text: target, engaged, opportunity, customer or churned
crm_contactA person at an account, also a core_person of type customer_contactperson_id → core_person; account_id → crm_account; first_name text req; last_name text req; title text; email text; phone text; owner_id → core_person; lifecycle_stage text: subscriber, lead, mql, sql, opportunity, customer, evangelist or other; do_not_contact bool req; source text
crm_leadAn unqualified prospect, kept as it arrivedfirst_name text; last_name text; company_name text; email text; phone text; source text; status text req: new, working, nurturing, qualified, unqualified or converted; owner_id → core_person; score num; qualification json; utm json; converted_contact_id → crm_contact; converted_account_id → crm_account; converted_opportunity_id → crm_opportunity; converted_at ts
crm_pipelineA named sales processname text req; object_type text req: opportunity or lead; is_default bool
crm_pipeline_stageA stage with its probability and exit rulepipeline_id → crm_pipeline req; name text req; seq int req; probability num req; is_won bool req; is_lost bool req; required_fields json
crm_opportunityA deal that can be won or lostname text req; account_id → crm_account req; primary_contact_id → crm_contact; pipeline_id → crm_pipeline req; stage_id → crm_pipeline_stage req; amount money; currency text; close_date date; probability num; forecast_category text req: pipeline, best_case, commit, closed or omitted; owner_id → core_person req; source text; lost_reason_code_id → core_reason_code; competitor_party_id → core_party; next_step text
crm_stage_historyOne row for every stage change; never updated or deletedopportunity_id → crm_opportunity req; from_stage_id → crm_pipeline_stage; to_stage_id → crm_pipeline_stage req; changed_at ts req; changed_by → core_person; amount_at_change money
crm_contact_roleA person's part in the buying group of a dealopportunity_id → crm_opportunity req; contact_id → crm_contact req; role text req: decision_maker, economic_buyer, champion, influencer, technical_evaluator, user or blocker
crm_activityA call, email, meeting, task, note, text or visit on any recordactivity_type text req: call, email, meeting, task, note, sms or site_visit; subject text req; body text; due_at ts; completed_at ts; owner_id → core_person; related_type text req; related_id uuid req, with no foreign key because it can point at any record; direction text: inbound, outbound or internal; outcome text; external_id text
crm_price_bookA price listname text req; currency text req; segment text; active bool req
crm_price_book_entryAn item's price in a price bookprice_book_id → crm_price_book req; item_id → core_item req; list_price money req; valid_from date; valid_to date
crm_quoteA quote, one row per versionquote_no text req, unique per tenant; version int req; opportunity_id → crm_opportunity; account_id → crm_account req; contact_id → crm_contact; status text req: draft, in_review, approved, sent, accepted, rejected, expired or superseded; valid_until date; currency text req; subtotal money; discount_total money; tax_total money; total money; terms text; approved_by → core_person; accepted_at ts; acceptance_ref text; pdf_attachment_id → core_attachment
crm_quote_lineA line of a quotequote_id → crm_quote req; line_no int req; item_id → core_item; description text req; qty qty req; uom_id → core_uom; unit_price money req; discount_pct num; line_total money; configuration json; lead_time_days int

Two tables in the specification are not built in this course: crm_cost_estimate (behind a quote line, explained in chapter 3) and crm_territory (name text req, rules json, owner_id → core_person). Add them later the same way.

The screens

Each screen is a row in the Role and Exposure Matrix with the roles that may open it, so the route guard and the menus follow from it. Simple one-table changes, such as claiming a lead or setting the discount threshold, are written straight to the table from the screen, under row-level security.

ScreenAddressWho opens itWhat it shows
Lead inbox/crm/leadsSales rep and sales managerLeads, new first, with claim, status, qualification and a convert button
Pipeline board/crm/pipelineSales rep and sales managerOne column for each stage (one stage at a time on a phone, with a stage picker); the manager also edits stages, probabilities and required fields
Quote builder/crm/quotesSales rep and sales managerItems from the price book with totals as the server computes them, status and versions; the manager also sees the approval queue and the discount threshold
Account lookup/crm/accountsSales rep, sales manager and service staffAccounts and contacts, with do_not_contact shown as a word; service staff see no deals, quotes or leads
Reports/crm/reportsSales managerPipeline as of a chosen date, win rate, cycle time and forecast by category; built in Session 6

Knowledge check

Why does the module store a crm_stage_history row for every stage change?

Knowledge check

Six deals are closed and four of them were won. What is the win rate by count?

References

  1. HubSpot developers: understanding the CRM. https://developers.hubspot.com/docs/api/crm/understanding-the-crm
  2. Metabase: shared sales CRM data model. https://www.metabase.com/integrations/sales-crm
  3. Syncari: HubSpot database schema compared with Salesforce. https://syncari.com/blog/hubspot-database-schema/

Chapter 3 · From price book to accepted order

Quotes, approvals and cost estimates

A quote is a promise with a price on it. This chapter shows how prices come from a price book on the server, how discounts get approved, how a changed quote becomes a new version, and how a shop that makes to order builds a price from a routing and a bill of materials.

25 minPrice on the server8 quote statusesCost ÷ (1 − margin)

By the end of this chapter you can

  • Price a quote from a price book and check the stored totals against a hand calculation.
  • Describe the quote statuses and the moves between them, including approval and versioning.
  • Say what acceptance records and what event it publishes for the sales order.
  • Build a unit price from cost and a target margin, and tell margin from markup.

Price books

A crm_price_book is a named price list with a currency, an optional segment (a customer group) and an active flag. Each crm_price_book_entry gives an item from the shared core_item list a list_price and, optionally, the dates it is valid from and to. Currencies are written as the three-letter codes in ISO 4217 [1]. Keeping prices in one book per currency or segment means a price change is one edit, and a customer-specific or distributor price is another book, not another spreadsheet.

Pricing is the server's job

A quote line holds an item, a description, a quantity, a unit of measure, a unit_price, an optional discount_pct, a line_total, any configuration as JSON and a lead_time_days. The browser may send only the item, the quantity and a discount percentage. The server reads the list price from the active entry valid today, copies it into the line as unit_price and works out every total. A price typed into a browser is never trusted, and because the quote keeps its own copy, a later price book change never alters a quote that was already sent.

Pricing a quoteBars to scale for a worked example. Line 1 is 10 at 25.00, which is 250.00. Line 2 is 4 at 100.00, which is 400.00. The subtotal is 650.00, the discount is 25.00 and the total is 625.00. The server reads the list price; the browser sends only item_id, qty and discount_pct.Line 1: 10 × 25.00250.00Line 2: 4 × 100.00400.00Subtotal650.00Discount: 10% of line 125.00Total625.00Bars share one scale. Discount 25.00 is 10% of 250.00; total = 650.00 − 25.00 = 625.00.
A worked example, to scale. Ten at 25.00 with 10% off line 1 and four at 100.00 give a subtotal of 650.00, a discount of 25.00 and a total of 625.00. The stored numbers must equal your hand calculation.
Quote fieldWhat it holds
quote_no, versionA unique number from the shared number sequence, taken in the same transaction so two people never get one number; version starts at 1
opportunity_id, account_id, contact_idThe deal, the customer (required) and who the quote is for
status, valid_until, currencyWhere the quote is, the date it stops being open, and its currency
subtotal, discount_total, tax_total, totalWorked out by the server; tax stays empty until you have a tax rule. line_total = qty x unit_price less discount_pct; subtotal = sum of qty x unit_price before discounts; discount_total = sum of line discounts; total = subtotal - discount_total (+ tax_total), which equals the sum of the line_totals
approved_by, accepted_at, acceptance_refWho approved it, when the customer accepted and the reference of the reply or signed PDF
pdf_attachment_idThe stored PDF of the quote as sent

Approval, versions and acceptance

A sales manager sets a discount threshold, a percentage that follows your own policy. If any line is above it, the quote cannot go straight to the customer: it moves from draft to in_review and creates an approval request. A manager approves or rejects with a comment, and the person who built the quote cannot approve it, which is the separation of duties auditors look for.

Quote statusesEight quote statuses. A draft goes to in_review, then approved, then sent, or straight to sent when no discount is above the threshold. In review a manager approves or rejects. A sent quote is accepted, rejected or superseded. Approved and rejected quotes can be superseded by a new version. Expired marks a quote past valid_until. no discount above the threshold: straight to sentdraftlines can changein_reviewwaits for a managerapprovedapproved_by is setsentPDF is storedrejectedwith a commentsupersededa newer version existsacceptedaccepted_at is setexpiredvalid_until has passedFrom sent, a quote is accepted, rejected or superseded. Lines of a quote that is not a draft never change.
Quote statuses. A quote is edited only while it is a draft. Approved, sent and rejected quotes become superseded when a newer version is made; a quote past valid_until is expired.

A changed quote is a new row, not an edit: version plus one, the number with -v2 on the end, the lines copied and the old row marked superseded. Sending stores a PDF of the quote and records its id. Acceptance is the customer saying yes, and the module records it: status accepted, the time from the server's clock and a reference to the customer's reply or signed PDF, and it publishes crm.quote.accepted. The event carries the quote number, the total, the currency and one entry for each line. When the order module is installed it turns the event into a sales order, and the module's definition of done requires the order's lines to match the quote.

Estimating a made-to-order price

A shop that makes parts to order does not have a list price for a part it has never made. It builds the price from cost. The crm_cost_estimate table sits behind a quote line and holds the material_cost from the bill of materials, labor_cost, machine_cost, overhead_cost, outside_cost for work sent out, the total_cost, the target_margin_pct and the assumptions as JSON. Its routing_ref is a soft link to the product definition from the manufacturing modules, so the estimate reuses the routing and the bill of materials instead of copying them.

Cost estimate to priceFive cost lines (material, labor, machine, overhead and outside processing) add up to total_cost. The total cost divided by the quantity is the unit cost; with target_margin_pct, the unit price is the unit cost divided by one minus the margin. material_costbill of materialslabor_cost(setup + run × qty) × ratemachine_costmachine time × machine rateoverhead_costyour overhead ruleoutside_costoutside processingtotal_costsum of the five,for the whole jobtarget_margin_pctunit_pricetotal_cost ÷ qty÷ (1 − margin)routing_ref links to the routing and bill of materials from M04 and M14. The estimate reuses them.
From routing and bill of materials to a price. Setup time is paid once per job and run time once per part, so labor is (setup + run × quantity) × rate. The five costs add up to the total cost of the job; divided by the quantity it is the unit cost, and the target margin turns that into a unit price.

Margin and markup are not the same. A margin is a share of the price; a markup is a share of the cost. For a cost of 80 and a target margin of 20%, the price is 80 ÷ (1 − 0.20) = 100, and the margin is 20 of 100. A markup of 20% on the same cost gives 96, which is a margin of only about 16.7%. These round numbers are for the arithmetic only; use your own rates and costs. Write the assumptions down with the estimate, so that whoever reads the quote later can tell what was assumed about scrap, setup and quantity.

Quote turnaround, the hours from request to quote sent, is the measure the specification calls key for job shops. An estimate built from the routing is how you shorten it without a spreadsheet per customer.

Exercise · Price one quote by hand, then make the system agree20 minutes

You need: One recent real quote from your business, a calculator, and your platform's quote screen or your AI coding agent

Use the numbers from a real quote. Do the arithmetic first, before the system does it.

Outcome: A hand calculation that equals the stored totals, a written approval threshold, a refused self-approval and a quote with two versions.

Knowledge check

A browser posts a unit price of 1.00 for an item whose price book entry is 25.00. What does the server store?

Knowledge check

A job costs 80 and the target margin is 20%. What is the price?

References

  1. ISO: ISO 4217, currency codes. https://www.iso.org/iso-4217-currency-codes.html
  2. Microsoft Learn: Common Data Model. https://learn.microsoft.com/en-us/common-data-model/
  3. Metabase: shared sales CRM data model. https://www.metabase.com/integrations/sales-crm
  4. HubSpot developers: understanding the CRM. https://developers.hubspot.com/docs/api/crm/understanding-the-crm

Chapter 4 · Roles, links and cutover

Access, events and moving in from your old CRM

The module only replaces your CRM when the right people can do their work in it, other modules hear what happens, and your old data comes across with its history intact. This chapter covers the roles, the events, the move and the cutover.

20 min4 roles and routes4 events outHistory in full

By the end of this chapter you can

  • List the roles the module needs and what each may do, and why the rights must cover every later session.
  • Name the events the module publishes and consumes.
  • Load accounts, contacts, deals, activities and history from an old CRM so reports continue unbroken.
  • Run old and new side by side, compare the numbers, and retire the subscription.

Who may do what

Row-level security (the database refusing rows a role should not see) is generated from the Role and Exposure Matrix, so a right that is missing from the matrix is refused later, in the middle of a session that needs it. Write every right the module needs, then walk each later step against the matrix before you build.

Role or routeMay doMay not
Lead intake routeInsert one crm_lead from the web form, with its utm data and consentRead a lead back, or set an owner
Sales repClaim leads, qualify and convert, move stages, log activities, build quotes and send themApprove any quote
Sales managerApprove or reject quotes, set the discount threshold and the stages, reassign work, run the import, read the team's pipeline and the reportsApprove a quote they built
Service staffRead accounts and contacts, and see the do_not_contact flagChange deals, quotes or prices

A customer has no login, so recording a customer's acceptance is done by a rep, or by a separate acceptance route with its own row in the matrix. Respecting do_not_contact is a rule in the database and on every screen and route that sends a message, not a courtesy.

Events in and out

Modules talk through events written to a table in the same transaction as the change (core_event_outbox), so an event cannot be lost if the change succeeds. The CRM publishes crm.lead.converted, crm.opportunity.stage_changed, crm.opportunity.won and crm.quote.accepted. It consumes cms.web_form.submitted, which becomes a lead, mkt.lead.mql from marketing, and svc.ticket.created, which feeds account health.

Events in and out of the CRMThree events come in: cms.web_form.submitted, mkt.lead.mql and svc.ticket.created. Four go out through core_event_outbox: crm.lead.converted, crm.opportunity.stage_changed, crm.opportunity.won and crm.quote.accepted, which M12 turns into a sales order. CONSUMESPUBLISHEScms.web_form.submitteda form post becomes a leadmkt.lead.mqla marketing-qualified leadsvc.ticket.createdaccount healthM15 CRMevents wait incore_event_outboxcrm.lead.convertedaccount, contact, opportunitycrm.opportunity.stage_changedwritten with each movecrm.opportunity.wonthe account becomes a customercrm.quote.acceptedM12 makes the sales order
The CRM's links to other modules. The accepted-quote event is the hand-off: its lines and total must match the sales order the order module creates.

Meetings and contacts also travel in standard file formats: a contact as a vCard [1] and a meeting as an iCalendar event [2]. They matter when you sync with Gmail or Outlook, or send an invitation, because every mail and calendar program reads them.

Moving the data in

Salesforce and HubSpot both export accounts or companies, contacts, deals or opportunities, activities and stage history through an API or as CSV files. Find out which of the five your own plan gives you, and read the vendor's export terms on your own account page. Exports hold personal data: keep them in a private folder outside the repository, and never paste them into a chat. Try everything on a throwaway copy of the database first.

Moving the data inEight steps: accounts, contacts, opportunities, stage history with original dates, contact roles, activities, leads, then reconcile the counts for every file. 1 · Accountscore_party + crm_account2 · Contactscore_person + crm_contact3 · Opportunitieswith owner and source4 · Stage historyoriginal dates, in full5 · Contact roleswho is on each deal6 · Activitiesexternal_id is the old id7 · Leadslast8 · Reconcilecounts add up per fileEvery row keeps ext.source_system and ext.source_id, so running the load twice changes nothing.Stage history lands in full before anyone uses the new CRM, so the numbers carry on without a gap.
The load order. Parents before children, because a deal needs its account and a history row needs its deal. The stage history keeps its original dates and is loaded in full before anyone uses the new CRM.
  1. Write the mapping. Company becomes a core_party and a crm_account; person becomes a core_person and a crm_contact; deal becomes an opportunity; each activity keeps the old id in external_id. Map every old user to an existing person, and every old stage and lost reason to yours, and stop on any that has no mapping.
  2. Carry opt-outs across. Anyone marked unsubscribed or do-not-contact in the old tool gets do_not_contact set to true. Copy consent only if the old tool holds a record of it.
  3. Write the duplicate rule first, as in chapter 1, and write skipped duplicates to a report with both source ids. The deals, contacts and activities that point at a skipped duplicate are loaded against the kept record, and an opt-out on a skipped copy is carried to the kept contact, so a person opted out on either copy stays do_not_contact.
  4. Keep the source of every record in ext.source_system and ext.source_id, with a unique index over them, so running the import twice skips what is already loaded. A stage change rarely has an id of its own, so a history row's source id is the old deal id, the stage and the time of the change joined together.
  5. Load history in full. If the old tool kept only a deal's current stage, load one starting row dated when the deal was created, mark it as having no history, and write down that analytics before the import date cannot be rebuilt.
  6. Reconcile. For each file write rows received, loaded, skipped as duplicates and refused, adding up to the file. Check that open pipeline at the export date equals the old tool's report.

Cut over and retire

Run the old tool and the new module side by side for at least a week. Compare open pipeline, win rate, cycle time and forecast, and explain every difference. When they agree, move the team over, cancel the subscription and record the saving: what the old tool cost a year, from its own bill. Costs are only what the specification and your own bills give; the module itself adds none beyond the platform you already run.

Deletion and anonymisation requests from the people in your data must also be handled without breaking the sales record. Under the GDPR [3] a person may ask for their personal data to be erased. The module supports this with a function the sales manager runs, crm_anonymise_contact, which replaces the name, email and phone of the contact, its core_person and any lead converted to it with the word Removed, sets do_not_contact and keeps the ids, the opportunities, the amounts and the stage history, so the pipeline and the win rate do not change. Free-text notes may still hold the person's details and are edited by hand. The audit log still holds them as well, because the audit trigger copied the old and new values of every change, names, emails and phones included, into the append-only core_audit_log, and so does any export or backup taken earlier; what to do about those is for the owner and their adviser to decide. What the law requires of your business in a given case is for you and your adviser to decide.

Exercise · Plan the move from your old CRM15 minutes

You need: Access to your old CRM's export page and its bill, and a text editor

Plan only; do not export anything yet. Look at what the tool offers and what you would map.

Outcome: A one-page migration plan with the export list, the stage and reason mapping, the duplicate rule, the reconciliation table and the cancellation date.

Knowledge check

Your old CRM exports deals with every stage change. How should you load them so reports carry on without a gap?

Knowledge check

A contact asks you to erase their personal data. What does the module keep?

References

  1. IETF RFC 6350: vCard Format Specification. https://www.rfc-editor.org/rfc/rfc6350
  2. IETF RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar). https://www.rfc-editor.org/rfc/rfc5545
  3. EUR-Lex: Regulation (EU) 2016/679, the General Data Protection Regulation. https://eur-lex.europa.eu/eli/reg/2016/679/oj
  4. Syncari: HubSpot database schema compared with Salesforce. https://syncari.com/blog/hubspot-database-schema/

Chapter 5 · 12 questions · 80% passes

Final assessment

Twelve questions across the element. Score 80% (10 of 12) to pass. Your LMS records your score and each answer; you can review the chapters and try again.

15 min12 questions≈ 15 minutesRetake allowed

Choose one answer for each question, then submit. You will see the right answer and why for every question.

1. What does crm_account.party_id do?
2. Which of these is a valid crm_lead status?
3. Why keep a stage history row for every stage change?
4. How is the open pipeline as of a past date found?
5. Which forecast category is set when a deal moves into a won stage?
6. How is win rate defined?
7. What does pipeline coverage compare?
8. Who decides the unit price on a quote line?
9. A sent quote is changed. What does the module do?
10. A job costs 60 and the target margin is 25%. What is the price?
11. Why does every imported row keep ext.source_system and ext.source_id with a unique index?
12. What may the lead intake route do?