From e16507d7167f3246b5621390aeb23ff81d4e8ce5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 04:08:01 +0000 Subject: [PATCH] refactor(crm): enforce-or-remove every decorative field, one verdict each (#1182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten declared fields had no business consumer. Nine are removed; one is kept and given the roll-up it always claimed to have. Removed, with every reader that went with them — view columns, filters and sorts, form sections, seed values, all four locale bundles, the contact import mapping AND its shipped CSV template, the developer field lists and the user docs in all three locales: crm_product.quantity_on_hand / reorder_point (+ the whole low_stock view, its switcher tab, and a "Low Stock" filter that compared against a hardcoded 10 rather than the reorder point beside it) crm_product.is_taxable, billing_type, unit_of_measure (+ 26 seeded values) crm_case.customer_signature, parent_case crm_task.estimated_hours, actual_hours crm_contact.reports_to, birthdate crm_campaign.parent_campaign Kept and ENFORCED: crm_account.parent_account. The accounts docs promised "roll-up reports — annual revenue of the global parent sums all children" and nothing computed it. child_account_revenue is that promise: a Field.summary over the self-referencing lookup, measured on the real engine across insert, child update, re-parent and delete before being declared. The same docs claimed cascading sharing, which crm_account (private) has never done; that claim is now corrected rather than half-fixed. Removal of published fields is authorized by the maintainer ruling of 2026-08-17, 逐个 enforce-or-remove(推荐), quoted in full in the PR body. test/decorative-field-sweep.test.ts pins both halves: the removals stay removed across every surface, and the roll-up is re-measured against the booted stack so a platform regression cannot turn the enforced row back into a decoration. Claude-Session: https://claude.ai/code/session_01XeAz8tSjwAibjdSDn9niiZ Co-Authored-By: Claude Opus 5 --- .changeset/decorative-field-sweep.md | 83 ++++++ assets/import-templates/contacts.csv | 102 ++++---- content/docs/administration/setup.mdx | 2 +- content/docs/administration/setup.zh-Hans.mdx | 2 +- content/docs/administration/setup.zh-Hant.mdx | 2 +- content/docs/guides/importing-your-data.mdx | 2 +- .../guides/importing-your-data.zh-Hans.mdx | 2 +- .../guides/importing-your-data.zh-Hant.mdx | 2 +- content/docs/revenue/billing-handoff.mdx | 2 +- .../docs/revenue/billing-handoff.zh-Hans.mdx | 2 +- .../docs/revenue/billing-handoff.zh-Hant.mdx | 2 +- content/docs/revenue/index.mdx | 4 +- content/docs/revenue/index.zh-Hans.mdx | 4 +- content/docs/revenue/index.zh-Hant.mdx | 4 +- content/docs/revenue/products.mdx | 43 +-- content/docs/revenue/products.zh-Hans.mdx | 43 +-- content/docs/revenue/products.zh-Hant.mdx | 43 +-- content/docs/sales/accounts.mdx | 7 +- content/docs/sales/accounts.zh-Hans.mdx | 7 +- content/docs/sales/accounts.zh-Hant.mdx | 7 +- content/docs/sales/activities.mdx | 2 +- content/docs/sales/activities.zh-Hans.mdx | 2 +- content/docs/sales/activities.zh-Hant.mdx | 2 +- content/docs/sales/contacts.mdx | 19 +- content/docs/sales/contacts.zh-Hans.mdx | 19 +- content/docs/sales/contacts.zh-Hant.mdx | 19 +- content/docs/service/cases.mdx | 20 +- content/docs/service/cases.zh-Hans.mdx | 20 +- content/docs/service/cases.zh-Hant.mdx | 20 +- docs/STATUS.md | 2 +- docs/developers/api_reference.md | 12 +- src/data/catalog.seed.ts | 26 -- src/mappings/contact_import.mapping.ts | 2 - src/objects/account.object.ts | 47 ++++ src/objects/campaign.object.ts | 13 +- src/objects/case.object.ts | 18 +- src/objects/contact.object.ts | 20 +- src/objects/product.object.ts | 59 +---- src/objects/task.object.ts | 18 +- src/translations/en.ts | 33 +-- src/translations/es-ES.ts | 30 +-- src/translations/ja-JP.ts | 30 +-- src/translations/zh-CN.ts | 30 +-- src/views/account.view.ts | 5 +- src/views/campaign.view.ts | 1 - src/views/case.view.ts | 3 +- src/views/contact.view.ts | 3 +- src/views/product.view.ts | 32 +-- src/views/task.view.ts | 2 - test/decorative-field-sweep.test.ts | 245 ++++++++++++++++++ 50 files changed, 596 insertions(+), 523 deletions(-) create mode 100644 .changeset/decorative-field-sweep.md create mode 100644 test/decorative-field-sweep.test.ts diff --git a/.changeset/decorative-field-sweep.md b/.changeset/decorative-field-sweep.md new file mode 100644 index 000000000..d97222228 --- /dev/null +++ b/.changeset/decorative-field-sweep.md @@ -0,0 +1,83 @@ +--- +'hotcrm': minor +--- + +Enforce-or-remove, decided per field: nine inert fields are gone, and the +account hierarchy is now a real roll-up. + +A consumer scan found ten declared fields that nothing in the product read or +wrote. Each got its own verdict rather than one verdict for the batch. The +common thread in the nine removals is that none of them looked absent — they +looked wired, which is the shape that misleads the next author into building on +them. + +**Removed from `crm_product`:** + +- **Quantity on Hand** and **Reorder Point**, with the **Low Stock** list view + and its **Low Stock** tab. Nothing ever decremented stock, and the view was + not the report it looked like: its filter compared quantity against a + hardcoded `10` rather than each product's own reorder point. HotCRM sells from + a catalog; stock belongs to the fulfilment system that owns it. +- **Taxable**. No quote or line item ever consulted it — + `crm_quote_line_item.tax_rate` defaults to `0` and is typed per line — so + clearing the flag on a zero-rated product changed no total anywhere. +- **Billing Type** and **Unit of Measure**, and the seeded values on all 13 + demo products. The docs said billing type "drives how the quote calculates + totals"; no quote total, revenue report or line-item behaviour ever read it. + +**Removed elsewhere:** + +- `crm_case.customer_signature` — a signature pad on the resolution form that + no close-case step, SLA measure or export ever read back. +- `crm_case.parent_case` — a case hierarchy nothing traversed: no rollup, no + cascading close, no related-case handling. +- `crm_task.estimated_hours` / `actual_hours` — no rollup summed them, no + variance report compared them, nothing warned when actual overran estimate. +- `crm_contact.reports_to` — the org chart the contact docs described as "a + clickable tree on the account detail page" that the Copilot "uses when + summarising the account". Neither existed: no page rendered it and no skill + read it. +- `crm_contact.birthdate` — importable, read by nothing. Personal data held for + no stated purpose is a liability rather than a feature. +- `crm_campaign.parent_campaign` — campaign hierarchy, mentioned in no doc and + read by nothing. Its honest consumer would be a rolled-up ROI, which cannot + be one declaration: `roi` is a formula over each campaign's own cost and + revenue, so a hierarchy ROI would put two differently-scoped ROI numbers on + one record. + +**Kept and enforced — `crm_account.parent_account`:** + +The hierarchy stays, and now does something. `crm_account` gains **Child Account +Revenue** (`child_account_revenue`), a roll-up of the **Annual Revenue** of an +account's **direct** children, maintained by the platform: it moves when a +child's revenue is edited, when a child is re-parented, and when a child is +deleted. This is the roll-up the accounts documentation already promised and +never had. It is one level deep — a grandparent totals its own children, not the +whole tree — and it is on the account form's **Financials** section. + +**Migration — what changes for users:** + +- **Product → Low Stock** view and tab are gone, as are the *Inventory + tracking*, *Billing types* and *Units of measure* sections of the product + documentation. Values previously stored in the five removed product fields are + dropped with the columns; export them first if your org typed anything into + them by hand. +- The **contact import template** no longer accepts a `Birthdate` column. A file + that still carries one will have that column ignored — re-cut your template + from *Guides → Importing your data*. The **account** template is unchanged and + still carries `Parent Account`. +- The case form loses **Parent Case** (SLA tab) and **Customer Signature** + (Resolution tab); the task form loses **Estimated Hours** and **Actual + Hours**; the contact form loses **Reports To** and **Birthday**; the campaign + form loses **Parent Campaign**. Any values held in those columns are dropped + with them. +- Accounts gain a read-only **Child Account Revenue** on the *Financials* + section. It is computed, not typed, and is blank (zero) for an account with no + children. +- The account hierarchy still does **not** cascade sharing — it never did, and + the accounts page said otherwise until now. Use the sharing rules under + *Administration → Sharing* for that. + +Authorized by the maintainer ruling of 2026-08-17, 「逐个 enforce-or-remove +(推荐)」: a declared-but-unenforced field ends the card either genuinely +enforced or genuinely gone. diff --git a/assets/import-templates/contacts.csv b/assets/import-templates/contacts.csv index be6e012a2..ee01a8e01 100644 --- a/assets/import-templates/contacts.csv +++ b/assets/import-templates/contacts.csv @@ -1,51 +1,51 @@ -Salutation,First Name,Last Name,Account Name,Title,Department,Email,Phone,Mobile,Mailing Street,Mailing City,Mailing State,Mailing Postal Code,Mailing Country,Birthdate,Lead Source,Contact Owner Email,Description -Mr.,Ana,Haddad,Northwind Traders,CEO,Executive,ana.haddad@northwindtraders.example.com,+1-555-0200,+1-555-0300,12 Harbour Way,Austin,TX,78701,USA,1970-01-10,Web,,Primary contact at Northwind Traders. -Ms.,Ben,Ibarra,Contoso Manufacturing,VP Sales,Sales,ben.ibarra@contosomanufacturing.example.com,+1-555-0201,+1-555-0301,480 Sequoia Ave,Portland,OR,97205,USA,1971-02-11,Referral,,Primary contact at Contoso Manufacturing. -Dr.,Chen,Jensen,Fabrikam Logistics,Head of Marketing,Marketing,chen.jensen@fabrikamlogistics.example.com,+1-555-0202,+1-555-0302,77 Kingsway,Boston,MA,02110,USA,1972-03-12,Trade Show,,Primary contact at Fabrikam Logistics. -Mrs.,Dara,Kaur,Adventure Works,CTO,Engineering,dara.kaur@adventureworks.example.com,+1-555-0203,+1-555-0303,1200 Market St,Denver,CO,80202,USA,1973-04-13,Webinar,,Primary contact at Adventure Works. -Prof.,Eli,Lindqvist,Litware Software,Procurement Manager,Support,eli.lindqvist@litwaresoftware.example.com,+1-555-0204,+1-555-0304,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,1974-05-14,Partner,,Primary contact at Litware Software. -Mr.,Farah,Moreau,Proseware Health,Operations Director,Finance,farah.moreau@prosewarehealth.example.com,+1-555-0205,+1-555-0305,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,1975-06-15,LinkedIn,,Primary contact at Proseware Health. -Ms.,Gus,Nakamura,Tailspin Toys,Finance Manager,HR,gus.nakamura@tailspintoys.example.com,+1-555-0206,+1-555-0306,65 Mill Lane,Munich,Bavaria,80331,Germany,1976-07-16,Google Ads,,Primary contact at Tailspin Toys. -Dr.,Hana,Okafor,Wide World Importers,IT Director,Operations,hana.okafor@wideworldimporters.example.com,+1-555-0207,+1-555-0307,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,1977-08-17,Cold Call,,Primary contact at Wide World Importers. -Mrs.,Ivan,Petrov,Woodgrove Bank,Support Lead,Executive,ivan.petrov@woodgrovebank.example.com,+1-555-0208,+1-555-0308,2100 Cedar Blvd,Singapore,Central,049315,Singapore,1978-09-18,Newsletter,,Primary contact at Woodgrove Bank. -Prof.,Jo,Quintero,Alpine Ski House,Founder,Sales,jo.quintero@alpineskihouse.example.com,+1-555-0209,+1-555-0309,41 Beacon Hill,Sydney,NSW,2000,Australia,1979-01-19,Blog,,Primary contact at Alpine Ski House. -Mr.,Kai,Rossi,Blue Yonder Airlines,CEO,Marketing,kai.rossi@blueyonderairlines.example.com,+1-555-0210,+1-555-0310,12 Harbour Way,Austin,TX,78701,USA,1980-02-10,Website,,Primary contact at Blue Yonder Airlines. -Ms.,Lena,Silva,Coho Vineyard,VP Sales,Engineering,lena.silva@cohovineyard.example.com,+1-555-0211,+1-555-0311,480 Sequoia Ave,Portland,OR,97205,USA,1981-03-11,Conference,,Primary contact at Coho Vineyard. -Dr.,Mira,Takahashi,Lucerne Publishing,Head of Marketing,Support,mira.takahashi@lucernepublishing.example.com,+1-555-0212,+1-555-0312,77 Kingsway,Boston,MA,02110,USA,1982-04-12,Web,,Primary contact at Lucerne Publishing. -Mrs.,Noel,Ueda,Trey Research,CTO,Finance,noel.ueda@treyresearch.example.com,+1-555-0213,+1-555-0313,1200 Market St,Denver,CO,80202,USA,1983-05-13,Referral,,Primary contact at Trey Research. -Prof.,Omar,Vargas,VanArsdel Design,Procurement Manager,HR,omar.vargas@vanarsdeldesign.example.com,+1-555-0214,+1-555-0314,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,1984-06-14,Trade Show,,Primary contact at VanArsdel Design. -Mr.,Pia,Weber,Relecloud Systems,Operations Director,Operations,pia.weber@relecloudsystems.example.com,+1-555-0215,+1-555-0315,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,1985-07-15,Webinar,,Primary contact at Relecloud Systems. -Ms.,Quinn,Xu,Bellows College,Finance Manager,Executive,quinn.xu@bellowscollege.example.com,+1-555-0216,+1-555-0316,65 Mill Lane,Munich,Bavaria,80331,Germany,1986-08-16,Partner,,Primary contact at Bellows College. -Dr.,Rosa,Yilmaz,Munson Pickles,IT Director,Sales,rosa.yilmaz@munsonpickles.example.com,+1-555-0217,+1-555-0317,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,1987-09-17,LinkedIn,,Primary contact at Munson Pickles. -Mrs.,Sam,Zhang,First Up Consultants,Support Lead,Marketing,sam.zhang@firstupconsultants.example.com,+1-555-0218,+1-555-0318,2100 Cedar Blvd,Singapore,Central,049315,Singapore,1988-01-18,Google Ads,,Primary contact at First Up Consultants. -Prof.,Tara,Abadi,Fourth Coffee,Founder,Engineering,tara.abadi@fourthcoffee.example.com,+1-555-0219,+1-555-0319,41 Beacon Hill,Sydney,NSW,2000,Australia,1989-02-19,Cold Call,,Primary contact at Fourth Coffee. -Mr.,Umar,Bianchi,Graphic Design Institute,CEO,Support,umar.bianchi@graphicdesigninstitute.example.com,+1-555-0220,+1-555-0320,12 Harbour Way,Austin,TX,78701,USA,1990-03-10,Newsletter,,Primary contact at Graphic Design Institute. -Ms.,Vera,Costa,Humongous Insurance,VP Sales,Finance,vera.costa@humongousinsurance.example.com,+1-555-0221,+1-555-0321,480 Sequoia Ave,Portland,OR,97205,USA,1991-04-11,Blog,,Primary contact at Humongous Insurance. -Dr.,Wes,Duarte,Margie Travel,Head of Marketing,HR,wes.duarte@margietravel.example.com,+1-555-0222,+1-555-0322,77 Kingsway,Boston,MA,02110,USA,1992-05-12,Website,,Primary contact at Margie Travel. -Mrs.,Xena,Eriksen,Nod Publishers,CTO,Operations,xena.eriksen@nodpublishers.example.com,+1-555-0223,+1-555-0323,1200 Market St,Denver,CO,80202,USA,1993-06-13,Conference,,Primary contact at Nod Publishers. -Prof.,Yuri,Fischer,Southridge Video,Procurement Manager,Executive,yuri.fischer@southridgevideo.example.com,+1-555-0224,+1-555-0324,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,1994-07-14,Web,,Primary contact at Southridge Video. -Mr.,Zoe,Grant,The Phone Company,Operations Director,Sales,zoe.grant@thephonecompany.example.com,+1-555-0225,+1-555-0325,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,1995-08-15,Referral,,Primary contact at The Phone Company. -Ms.,Alma,Horvath,City Power & Light,Finance Manager,Marketing,alma.horvath@citypowerlight.example.com,+1-555-0226,+1-555-0326,65 Mill Lane,Munich,Bavaria,80331,Germany,1996-09-16,Trade Show,,Primary contact at City Power & Light. -Dr.,Bruno,Ivanov,Consolidated Messenger,IT Director,Engineering,bruno.ivanov@consolidatedmessenger.example.com,+1-555-0227,+1-555-0327,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,1997-01-17,Webinar,,Primary contact at Consolidated Messenger. -Mrs.,Cleo,Jonsson,Fincher Architects,Support Lead,Support,cleo.jonsson@fincherarchitects.example.com,+1-555-0228,+1-555-0328,2100 Cedar Blvd,Singapore,Central,049315,Singapore,1998-02-18,Partner,,Primary contact at Fincher Architects. -Prof.,Dev,Klein,Lamna Healthcare,Founder,Finance,dev.klein@lamnahealthcare.example.com,+1-555-0229,+1-555-0329,41 Beacon Hill,Sydney,NSW,2000,Australia,1999-03-19,LinkedIn,,Primary contact at Lamna Healthcare. -Mr.,Elsa,Larsen,Liberty Delightful Sinful Bakery,CEO,HR,elsa.larsen@libertydelightfulsinfulb.example.com,+1-555-0230,+1-555-0330,12 Harbour Way,Austin,TX,78701,USA,1970-04-10,Google Ads,,Primary contact at Liberty Delightful Sinful Bakery. -Ms.,Finn,Mensah,Best For You Organics,VP Sales,Operations,finn.mensah@bestforyouorganics.example.com,+1-555-0231,+1-555-0331,480 Sequoia Ave,Portland,OR,97205,USA,1971-05-11,Cold Call,,Primary contact at Best For You Organics. -Dr.,Gia,Novak,Wingtip Toys,Head of Marketing,Executive,gia.novak@wingtiptoys.example.com,+1-555-0232,+1-555-0332,77 Kingsway,Boston,MA,02110,USA,1972-06-12,Newsletter,,Primary contact at Wingtip Toys. -Mrs.,Hugo,Ortiz,Contoso Energy,CTO,Sales,hugo.ortiz@contosoenergy.example.com,+1-555-0233,+1-555-0333,1200 Market St,Denver,CO,80202,USA,1973-07-13,Blog,,Primary contact at Contoso Energy. -Prof.,Iris,Pires,Datum Analytics,Procurement Manager,Marketing,iris.pires@datumanalytics.example.com,+1-555-0234,+1-555-0334,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,1974-08-14,Website,,Primary contact at Datum Analytics. -Mr.,Jonas,Rahman,Fabrikam Residences,Operations Director,Engineering,jonas.rahman@fabrikamresidences.example.com,+1-555-0235,+1-555-0335,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,1975-09-15,Conference,,Primary contact at Fabrikam Residences. -Ms.,Kira,Sorensen,Nordwind Capital,Finance Manager,Support,kira.sorensen@nordwindcapital.example.com,+1-555-0236,+1-555-0336,65 Mill Lane,Munich,Bavaria,80331,Germany,1976-01-16,Web,,Primary contact at Nordwind Capital. -Dr.,Liam,Thorne,Tailwind Traders,IT Director,Finance,liam.thorne@tailwindtraders.example.com,+1-555-0237,+1-555-0337,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,1977-02-17,Referral,,Primary contact at Tailwind Traders. -Mrs.,Maya,Ulrich,Bellows Robotics,Support Lead,HR,maya.ulrich@bellowsrobotics.example.com,+1-555-0238,+1-555-0338,2100 Cedar Blvd,Singapore,Central,049315,Singapore,1978-03-18,Trade Show,,Primary contact at Bellows Robotics. -Prof.,Nils,Voss,Cronus Pharma,Founder,Operations,nils.voss@cronuspharma.example.com,+1-555-0239,+1-555-0339,41 Beacon Hill,Sydney,NSW,2000,Australia,1979-04-19,Webinar,,Primary contact at Cronus Pharma. -Mr.,Olive,Walsh,Halo Public Works,CEO,Executive,olive.walsh@halopublicworks.example.com,+1-555-0240,+1-555-0340,12 Harbour Way,Austin,TX,78701,USA,1980-05-10,Partner,,Primary contact at Halo Public Works. -Ms.,Paolo,Yoon,Helping Hands Foundation,VP Sales,Sales,paolo.yoon@helpinghandsfoundation.example.com,+1-555-0241,+1-555-0341,480 Sequoia Ave,Portland,OR,97205,USA,1981-06-11,LinkedIn,,Primary contact at Helping Hands Foundation. -Dr.,Rhea,Zielinski,Iride Motors,Head of Marketing,Marketing,rhea.zielinski@iridemotors.example.com,+1-555-0242,+1-555-0342,77 Kingsway,Boston,MA,02110,USA,1982-07-12,Google Ads,,Primary contact at Iride Motors. -Mrs.,Soren,Alvarez,Kiana Freight,CTO,Engineering,soren.alvarez@kianafreight.example.com,+1-555-0243,+1-555-0343,1200 Market St,Denver,CO,80202,USA,1983-08-13,Cold Call,,Primary contact at Kiana Freight. -Prof.,Tessa,Baker,Lakeview Medical,Procurement Manager,Support,tessa.baker@lakeviewmedical.example.com,+1-555-0244,+1-555-0344,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,1984-09-14,Newsletter,,Primary contact at Lakeview Medical. -Mr.,Uma,Chen,Meridian Legal,Operations Director,Finance,uma.chen@meridianlegal.example.com,+1-555-0245,+1-555-0345,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,1985-01-15,Blog,,Primary contact at Meridian Legal. -Ms.,Viktor,Delgado,Orion Semiconductor,Finance Manager,HR,viktor.delgado@orionsemiconductor.example.com,+1-555-0246,+1-555-0346,65 Mill Lane,Munich,Bavaria,80331,Germany,1986-02-16,Website,,Primary contact at Orion Semiconductor. -Dr.,Wren,Egan,Pinewood Estates,IT Director,Operations,wren.egan@pinewoodestates.example.com,+1-555-0247,+1-555-0347,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,1987-03-17,Conference,,Primary contact at Pinewood Estates. -Mrs.,Yasmin,Ferrara,Quill Stationery,Support Lead,Executive,yasmin.ferrara@quillstationery.example.com,+1-555-0248,+1-555-0348,2100 Cedar Blvd,Singapore,Central,049315,Singapore,1988-04-18,Web,,Primary contact at Quill Stationery. -Prof.,Zane,Gomez,Redwood Renewables,Founder,Sales,zane.gomez@redwoodrenewables.example.com,+1-555-0249,+1-555-0349,41 Beacon Hill,Sydney,NSW,2000,Australia,1989-05-19,Referral,,Primary contact at Redwood Renewables. +Salutation,First Name,Last Name,Account Name,Title,Department,Email,Phone,Mobile,Mailing Street,Mailing City,Mailing State,Mailing Postal Code,Mailing Country,Lead Source,Contact Owner Email,Description +Mr.,Ana,Haddad,Northwind Traders,CEO,Executive,ana.haddad@northwindtraders.example.com,+1-555-0200,+1-555-0300,12 Harbour Way,Austin,TX,78701,USA,Web,,Primary contact at Northwind Traders. +Ms.,Ben,Ibarra,Contoso Manufacturing,VP Sales,Sales,ben.ibarra@contosomanufacturing.example.com,+1-555-0201,+1-555-0301,480 Sequoia Ave,Portland,OR,97205,USA,Referral,,Primary contact at Contoso Manufacturing. +Dr.,Chen,Jensen,Fabrikam Logistics,Head of Marketing,Marketing,chen.jensen@fabrikamlogistics.example.com,+1-555-0202,+1-555-0302,77 Kingsway,Boston,MA,02110,USA,Trade Show,,Primary contact at Fabrikam Logistics. +Mrs.,Dara,Kaur,Adventure Works,CTO,Engineering,dara.kaur@adventureworks.example.com,+1-555-0203,+1-555-0303,1200 Market St,Denver,CO,80202,USA,Webinar,,Primary contact at Adventure Works. +Prof.,Eli,Lindqvist,Litware Software,Procurement Manager,Support,eli.lindqvist@litwaresoftware.example.com,+1-555-0204,+1-555-0304,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,Partner,,Primary contact at Litware Software. +Mr.,Farah,Moreau,Proseware Health,Operations Director,Finance,farah.moreau@prosewarehealth.example.com,+1-555-0205,+1-555-0305,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,LinkedIn,,Primary contact at Proseware Health. +Ms.,Gus,Nakamura,Tailspin Toys,Finance Manager,HR,gus.nakamura@tailspintoys.example.com,+1-555-0206,+1-555-0306,65 Mill Lane,Munich,Bavaria,80331,Germany,Google Ads,,Primary contact at Tailspin Toys. +Dr.,Hana,Okafor,Wide World Importers,IT Director,Operations,hana.okafor@wideworldimporters.example.com,+1-555-0207,+1-555-0307,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,Cold Call,,Primary contact at Wide World Importers. +Mrs.,Ivan,Petrov,Woodgrove Bank,Support Lead,Executive,ivan.petrov@woodgrovebank.example.com,+1-555-0208,+1-555-0308,2100 Cedar Blvd,Singapore,Central,049315,Singapore,Newsletter,,Primary contact at Woodgrove Bank. +Prof.,Jo,Quintero,Alpine Ski House,Founder,Sales,jo.quintero@alpineskihouse.example.com,+1-555-0209,+1-555-0309,41 Beacon Hill,Sydney,NSW,2000,Australia,Blog,,Primary contact at Alpine Ski House. +Mr.,Kai,Rossi,Blue Yonder Airlines,CEO,Marketing,kai.rossi@blueyonderairlines.example.com,+1-555-0210,+1-555-0310,12 Harbour Way,Austin,TX,78701,USA,Website,,Primary contact at Blue Yonder Airlines. +Ms.,Lena,Silva,Coho Vineyard,VP Sales,Engineering,lena.silva@cohovineyard.example.com,+1-555-0211,+1-555-0311,480 Sequoia Ave,Portland,OR,97205,USA,Conference,,Primary contact at Coho Vineyard. +Dr.,Mira,Takahashi,Lucerne Publishing,Head of Marketing,Support,mira.takahashi@lucernepublishing.example.com,+1-555-0212,+1-555-0312,77 Kingsway,Boston,MA,02110,USA,Web,,Primary contact at Lucerne Publishing. +Mrs.,Noel,Ueda,Trey Research,CTO,Finance,noel.ueda@treyresearch.example.com,+1-555-0213,+1-555-0313,1200 Market St,Denver,CO,80202,USA,Referral,,Primary contact at Trey Research. +Prof.,Omar,Vargas,VanArsdel Design,Procurement Manager,HR,omar.vargas@vanarsdeldesign.example.com,+1-555-0214,+1-555-0314,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,Trade Show,,Primary contact at VanArsdel Design. +Mr.,Pia,Weber,Relecloud Systems,Operations Director,Operations,pia.weber@relecloudsystems.example.com,+1-555-0215,+1-555-0315,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,Webinar,,Primary contact at Relecloud Systems. +Ms.,Quinn,Xu,Bellows College,Finance Manager,Executive,quinn.xu@bellowscollege.example.com,+1-555-0216,+1-555-0316,65 Mill Lane,Munich,Bavaria,80331,Germany,Partner,,Primary contact at Bellows College. +Dr.,Rosa,Yilmaz,Munson Pickles,IT Director,Sales,rosa.yilmaz@munsonpickles.example.com,+1-555-0217,+1-555-0317,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,LinkedIn,,Primary contact at Munson Pickles. +Mrs.,Sam,Zhang,First Up Consultants,Support Lead,Marketing,sam.zhang@firstupconsultants.example.com,+1-555-0218,+1-555-0318,2100 Cedar Blvd,Singapore,Central,049315,Singapore,Google Ads,,Primary contact at First Up Consultants. +Prof.,Tara,Abadi,Fourth Coffee,Founder,Engineering,tara.abadi@fourthcoffee.example.com,+1-555-0219,+1-555-0319,41 Beacon Hill,Sydney,NSW,2000,Australia,Cold Call,,Primary contact at Fourth Coffee. +Mr.,Umar,Bianchi,Graphic Design Institute,CEO,Support,umar.bianchi@graphicdesigninstitute.example.com,+1-555-0220,+1-555-0320,12 Harbour Way,Austin,TX,78701,USA,Newsletter,,Primary contact at Graphic Design Institute. +Ms.,Vera,Costa,Humongous Insurance,VP Sales,Finance,vera.costa@humongousinsurance.example.com,+1-555-0221,+1-555-0321,480 Sequoia Ave,Portland,OR,97205,USA,Blog,,Primary contact at Humongous Insurance. +Dr.,Wes,Duarte,Margie Travel,Head of Marketing,HR,wes.duarte@margietravel.example.com,+1-555-0222,+1-555-0322,77 Kingsway,Boston,MA,02110,USA,Website,,Primary contact at Margie Travel. +Mrs.,Xena,Eriksen,Nod Publishers,CTO,Operations,xena.eriksen@nodpublishers.example.com,+1-555-0223,+1-555-0323,1200 Market St,Denver,CO,80202,USA,Conference,,Primary contact at Nod Publishers. +Prof.,Yuri,Fischer,Southridge Video,Procurement Manager,Executive,yuri.fischer@southridgevideo.example.com,+1-555-0224,+1-555-0324,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,Web,,Primary contact at Southridge Video. +Mr.,Zoe,Grant,The Phone Company,Operations Director,Sales,zoe.grant@thephonecompany.example.com,+1-555-0225,+1-555-0325,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,Referral,,Primary contact at The Phone Company. +Ms.,Alma,Horvath,City Power & Light,Finance Manager,Marketing,alma.horvath@citypowerlight.example.com,+1-555-0226,+1-555-0326,65 Mill Lane,Munich,Bavaria,80331,Germany,Trade Show,,Primary contact at City Power & Light. +Dr.,Bruno,Ivanov,Consolidated Messenger,IT Director,Engineering,bruno.ivanov@consolidatedmessenger.example.com,+1-555-0227,+1-555-0327,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,Webinar,,Primary contact at Consolidated Messenger. +Mrs.,Cleo,Jonsson,Fincher Architects,Support Lead,Support,cleo.jonsson@fincherarchitects.example.com,+1-555-0228,+1-555-0328,2100 Cedar Blvd,Singapore,Central,049315,Singapore,Partner,,Primary contact at Fincher Architects. +Prof.,Dev,Klein,Lamna Healthcare,Founder,Finance,dev.klein@lamnahealthcare.example.com,+1-555-0229,+1-555-0329,41 Beacon Hill,Sydney,NSW,2000,Australia,LinkedIn,,Primary contact at Lamna Healthcare. +Mr.,Elsa,Larsen,Liberty Delightful Sinful Bakery,CEO,HR,elsa.larsen@libertydelightfulsinfulb.example.com,+1-555-0230,+1-555-0330,12 Harbour Way,Austin,TX,78701,USA,Google Ads,,Primary contact at Liberty Delightful Sinful Bakery. +Ms.,Finn,Mensah,Best For You Organics,VP Sales,Operations,finn.mensah@bestforyouorganics.example.com,+1-555-0231,+1-555-0331,480 Sequoia Ave,Portland,OR,97205,USA,Cold Call,,Primary contact at Best For You Organics. +Dr.,Gia,Novak,Wingtip Toys,Head of Marketing,Executive,gia.novak@wingtiptoys.example.com,+1-555-0232,+1-555-0332,77 Kingsway,Boston,MA,02110,USA,Newsletter,,Primary contact at Wingtip Toys. +Mrs.,Hugo,Ortiz,Contoso Energy,CTO,Sales,hugo.ortiz@contosoenergy.example.com,+1-555-0233,+1-555-0333,1200 Market St,Denver,CO,80202,USA,Blog,,Primary contact at Contoso Energy. +Prof.,Iris,Pires,Datum Analytics,Procurement Manager,Marketing,iris.pires@datumanalytics.example.com,+1-555-0234,+1-555-0334,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,Website,,Primary contact at Datum Analytics. +Mr.,Jonas,Rahman,Fabrikam Residences,Operations Director,Engineering,jonas.rahman@fabrikamresidences.example.com,+1-555-0235,+1-555-0335,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,Conference,,Primary contact at Fabrikam Residences. +Ms.,Kira,Sorensen,Nordwind Capital,Finance Manager,Support,kira.sorensen@nordwindcapital.example.com,+1-555-0236,+1-555-0336,65 Mill Lane,Munich,Bavaria,80331,Germany,Web,,Primary contact at Nordwind Capital. +Dr.,Liam,Thorne,Tailwind Traders,IT Director,Finance,liam.thorne@tailwindtraders.example.com,+1-555-0237,+1-555-0337,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,Referral,,Primary contact at Tailwind Traders. +Mrs.,Maya,Ulrich,Bellows Robotics,Support Lead,HR,maya.ulrich@bellowsrobotics.example.com,+1-555-0238,+1-555-0338,2100 Cedar Blvd,Singapore,Central,049315,Singapore,Trade Show,,Primary contact at Bellows Robotics. +Prof.,Nils,Voss,Cronus Pharma,Founder,Operations,nils.voss@cronuspharma.example.com,+1-555-0239,+1-555-0339,41 Beacon Hill,Sydney,NSW,2000,Australia,Webinar,,Primary contact at Cronus Pharma. +Mr.,Olive,Walsh,Halo Public Works,CEO,Executive,olive.walsh@halopublicworks.example.com,+1-555-0240,+1-555-0340,12 Harbour Way,Austin,TX,78701,USA,Partner,,Primary contact at Halo Public Works. +Ms.,Paolo,Yoon,Helping Hands Foundation,VP Sales,Sales,paolo.yoon@helpinghandsfoundation.example.com,+1-555-0241,+1-555-0341,480 Sequoia Ave,Portland,OR,97205,USA,LinkedIn,,Primary contact at Helping Hands Foundation. +Dr.,Rhea,Zielinski,Iride Motors,Head of Marketing,Marketing,rhea.zielinski@iridemotors.example.com,+1-555-0242,+1-555-0342,77 Kingsway,Boston,MA,02110,USA,Google Ads,,Primary contact at Iride Motors. +Mrs.,Soren,Alvarez,Kiana Freight,CTO,Engineering,soren.alvarez@kianafreight.example.com,+1-555-0243,+1-555-0343,1200 Market St,Denver,CO,80202,USA,Cold Call,,Primary contact at Kiana Freight. +Prof.,Tessa,Baker,Lakeview Medical,Procurement Manager,Support,tessa.baker@lakeviewmedical.example.com,+1-555-0244,+1-555-0344,9 Riverside Terrace,Toronto,ON,M5H 2N2,Canada,Newsletter,,Primary contact at Lakeview Medical. +Mr.,Uma,Chen,Meridian Legal,Operations Director,Finance,uma.chen@meridianlegal.example.com,+1-555-0245,+1-555-0345,350 Alder Road,Manchester,Greater Manchester,M1 2AB,United Kingdom,Blog,,Primary contact at Meridian Legal. +Ms.,Viktor,Delgado,Orion Semiconductor,Finance Manager,HR,viktor.delgado@orionsemiconductor.example.com,+1-555-0246,+1-555-0346,65 Mill Lane,Munich,Bavaria,80331,Germany,Website,,Primary contact at Orion Semiconductor. +Dr.,Wren,Egan,Pinewood Estates,IT Director,Operations,wren.egan@pinewoodestates.example.com,+1-555-0247,+1-555-0347,18 Copper Court,Lyon,Auvergne-Rhône-Alpes,69002,France,Conference,,Primary contact at Pinewood Estates. +Mrs.,Yasmin,Ferrara,Quill Stationery,Support Lead,Executive,yasmin.ferrara@quillstationery.example.com,+1-555-0248,+1-555-0348,2100 Cedar Blvd,Singapore,Central,049315,Singapore,Web,,Primary contact at Quill Stationery. +Prof.,Zane,Gomez,Redwood Renewables,Founder,Sales,zane.gomez@redwoodrenewables.example.com,+1-555-0249,+1-555-0349,41 Beacon Hill,Sydney,NSW,2000,Australia,Referral,,Primary contact at Redwood Renewables. diff --git a/content/docs/administration/setup.mdx b/content/docs/administration/setup.mdx index 63c477ca2..e6838a54f 100644 --- a/content/docs/administration/setup.mdx +++ b/content/docs/administration/setup.mdx @@ -72,7 +72,7 @@ What follows from that is on the SLA side: deadlines run on **calendar hours**. ### 9. Product catalog -- [ ] Import [products](/docs/revenue/products) — at minimum: code, name, list price, billing type +- [ ] Import [products](/docs/revenue/products) — at minimum: code, name, list price - [ ] Set up product manager (the contact for each product line) - [ ] Upload datasheets — a PDF file field on the product record, there for people to open; no knowledge base ingests it diff --git a/content/docs/administration/setup.zh-Hans.mdx b/content/docs/administration/setup.zh-Hans.mdx index 1949fbfa4..f03e56f85 100644 --- a/content/docs/administration/setup.zh-Hans.mdx +++ b/content/docs/administration/setup.zh-Hans.mdx @@ -72,7 +72,7 @@ description: 让 HotCRM 为你的团队做好生产准备的第一周任务。 ### 9. 产品目录 -- [ ] 导入 [产品](/zh-Hans/docs/revenue/products) —— 至少:代码、名称、标价、计费类型 +- [ ] 导入 [产品](/zh-Hans/docs/revenue/products) —— 至少:代码、名称、标价 - [ ] 设置产品经理(每条产品线的联系人) - [ ] 上传数据表 —— 产品记录上的一个 PDF 文件字段,供人打开查看;没有任何知识库会摄取它 diff --git a/content/docs/administration/setup.zh-Hant.mdx b/content/docs/administration/setup.zh-Hant.mdx index 1ce8b226f..51111aa3f 100644 --- a/content/docs/administration/setup.zh-Hant.mdx +++ b/content/docs/administration/setup.zh-Hant.mdx @@ -72,7 +72,7 @@ description: 讓 HotCRM 為你的團隊做好生產準備的第一週任務。 ### 9. 產品目錄 -- [ ] 匯入 [產品](/zh-Hant/docs/revenue/products) —— 至少:代碼、名稱、標價、計費類型 +- [ ] 匯入 [產品](/zh-Hant/docs/revenue/products) —— 至少:代碼、名稱、標價 - [ ] 設置產品經理(每條產品線的聯絡人) - [ ] 上傳資料表 —— 產品記錄上的一個 PDF 檔案欄位,供人開啟檢視;沒有任何知識庫會攝取它 diff --git a/content/docs/guides/importing-your-data.mdx b/content/docs/guides/importing-your-data.mdx index c840dde36..2305dd9f0 100644 --- a/content/docs/guides/importing-your-data.mdx +++ b/content/docs/guides/importing-your-data.mdx @@ -203,7 +203,7 @@ file. *Website* must be a full URL (`https://…`) or the row is rejected. `Salutation` · `First Name` · `Last Name` · `Account Name` · `Title` · `Department` · `Email` · `Phone` · `Mobile` · `Mailing Street` · `Mailing City` · -`Mailing State` · `Mailing Postal Code` · `Mailing Country` · `Birthdate` · +`Mailing State` · `Mailing Postal Code` · `Mailing Country` · `Lead Source` · `Contact Owner Email` · `Description` *First Name*, *Last Name*, *Email* and *Account Name* are required. diff --git a/content/docs/guides/importing-your-data.zh-Hans.mdx b/content/docs/guides/importing-your-data.zh-Hans.mdx index b2aead723..895c0b32e 100644 --- a/content/docs/guides/importing-your-data.zh-Hans.mdx +++ b/content/docs/guides/importing-your-data.zh-Hans.mdx @@ -182,7 +182,7 @@ curl -X POST "$HOTCRM/api/v1/data/import/jobs/$JOB_ID/undo" -H "Authorization: B `Salutation` · `First Name` · `Last Name` · `Account Name` · `Title` · `Department` · `Email` · `Phone` · `Mobile` · `Mailing Street` · `Mailing City` · -`Mailing State` · `Mailing Postal Code` · `Mailing Country` · `Birthdate` · +`Mailing State` · `Mailing Postal Code` · `Mailing Country` · `Lead Source` · `Contact Owner Email` · `Description` *First Name*、*Last Name*、*Email* 和 *Account Name* 为必填。 diff --git a/content/docs/guides/importing-your-data.zh-Hant.mdx b/content/docs/guides/importing-your-data.zh-Hant.mdx index 45ea12383..03205ec4d 100644 --- a/content/docs/guides/importing-your-data.zh-Hant.mdx +++ b/content/docs/guides/importing-your-data.zh-Hant.mdx @@ -185,7 +185,7 @@ curl -X POST "$HOTCRM/api/v1/data/import/jobs/$JOB_ID/undo" -H "Authorization: B `Salutation` · `First Name` · `Last Name` · `Account Name` · `Title` · `Department` · `Email` · `Phone` · `Mobile` · `Mailing Street` · `Mailing City` · -`Mailing State` · `Mailing Postal Code` · `Mailing Country` · `Birthdate` · +`Mailing State` · `Mailing Postal Code` · `Mailing Country` · `Lead Source` · `Contact Owner Email` · `Description` *First Name*、*Last Name*、*Email* 與 *Account Name* 為必填。 diff --git a/content/docs/revenue/billing-handoff.mdx b/content/docs/revenue/billing-handoff.mdx index 29d95b764..0595cde42 100644 --- a/content/docs/revenue/billing-handoff.mdx +++ b/content/docs/revenue/billing-handoff.mdx @@ -22,7 +22,7 @@ That is a deliberate boundary, not a gap. Billing, tax, collections and revenue Two consequences worth stating plainly, because people look for them: -- **`Quantity on Hand` on a product is display-only.** Closing a deal does not decrement it. Stock movement belongs to the fulfilment system. +- **HotCRM holds no stock figure at all.** A product carries no quantity on hand, and closing a deal decrements nothing. Inventory is the fulfilment system’s record, and this app deliberately keeps no second copy of it. - **Payment status is never mirrored back.** Nothing in HotCRM tells you whether an invoice was paid. A future inbound integration could, but it does not exist today, and no field pretends otherwise. ## The two events diff --git a/content/docs/revenue/billing-handoff.zh-Hans.mdx b/content/docs/revenue/billing-handoff.zh-Hans.mdx index 80576f97b..f94f93db0 100644 --- a/content/docs/revenue/billing-handoff.zh-Hans.mdx +++ b/content/docs/revenue/billing-handoff.zh-Hans.mdx @@ -22,7 +22,7 @@ HotCRM 负责营收的**商务**一侧:找到客户、谈定价格、签下合 有两点值得明说,因为总有人来找: -- **产品上的「库存数量」只用于展示。** 赢单不会扣减它。库存变动属于履约系统。 +- **HotCRM 根本不保存库存数字。** 产品上没有库存数量字段,赢单也不会扣减任何东西。库存是履约系统的记录,本应用刻意不保留第二份副本。 - **回款状态永远不会写回来。** HotCRM 里没有任何字段告诉你发票是否已付。未来的入站集成可以做到这件事,但今天它不存在,也没有字段假装它存在。 ## 两个事件 diff --git a/content/docs/revenue/billing-handoff.zh-Hant.mdx b/content/docs/revenue/billing-handoff.zh-Hant.mdx index f2f842864..1e2ffbcbb 100644 --- a/content/docs/revenue/billing-handoff.zh-Hant.mdx +++ b/content/docs/revenue/billing-handoff.zh-Hant.mdx @@ -22,7 +22,7 @@ HotCRM 負責營收的**商務**一側:找到客戶、談定價格、簽下合 有兩點值得明說,因為總有人來找: -- **產品上的「庫存數量」只用於顯示。** 贏單不會扣減它。庫存異動屬於履約系統。 +- **HotCRM 根本不保存庫存數字。** 產品上沒有庫存數量欄位,贏單也不會扣減任何東西。庫存是履約系統的記錄,本應用刻意不保留第二份副本。 - **收款狀態永遠不會寫回來。** HotCRM 裡沒有任何欄位告訴你發票是否已付。未來的入站整合可以做到這件事,但今天它不存在,也沒有欄位假裝它存在。 ## 兩個事件 diff --git a/content/docs/revenue/index.mdx b/content/docs/revenue/index.mdx index 5a36ef0e4..da8b90bd1 100644 --- a/content/docs/revenue/index.mdx +++ b/content/docs/revenue/index.mdx @@ -11,7 +11,7 @@ The Revenue module covers everything that happens **after sales says "we won"**: | Area | What it does | | --- | --- | -| [**Products**](/docs/revenue/products) | The catalog of what you sell — names, codes, list prices, billing types. | +| [**Products**](/docs/revenue/products) | The catalog of what you sell — names, codes, list prices, SKUs. | | [**Contracts**](/docs/revenue/contracts) | Signed agreements with customers — terms, value, renewals. | | [**Approvals**](/docs/revenue/approvals) | Multi-step approval for high-value discounts. | | [**Billing Hand-off**](/docs/revenue/billing-handoff) | Where CRM scope ends — the two outbound events that hand a won deal and an activated contract to billing. | @@ -31,7 +31,7 @@ Contract (Activated) Renewal task ──► Renewal opportunity ``` -1. **Set up the catalog** — Products with codes, list prices, billing types. +1. **Set up the catalog** — Products with codes, list prices and SKUs. 2. **Build quotes** with line items from the catalog (see [Quotes](/docs/sales/quotes)). 3. **On acceptance** the system creates a contract with the quote's terms. 4. **Contract is activated** — the account is promoted to *Customer*. diff --git a/content/docs/revenue/index.zh-Hans.mdx b/content/docs/revenue/index.zh-Hans.mdx index 8a96b1b92..af7ad040e 100644 --- a/content/docs/revenue/index.zh-Hans.mdx +++ b/content/docs/revenue/index.zh-Hans.mdx @@ -11,7 +11,7 @@ description: 产品目录、合同生命周期、续约,以及高价值折扣 | 领域 | 作用 | | --- | --- | -| [**产品**](/zh-Hans/docs/revenue/products) | 你所销售内容的目录——名称、编码、标价、计费类型。 | +| [**产品**](/zh-Hans/docs/revenue/products) | 你所销售内容的目录——名称、编码、标价、SKU。 | | [**合同**](/zh-Hans/docs/revenue/contracts) | 与客户签署的协议——条款、价值、续约。 | | [**审批**](/zh-Hans/docs/revenue/approvals) | 针对高价值折扣的多步骤审批。 | | [**计费交接**](/zh-Hans/docs/revenue/billing-handoff) | CRM 范围的终点——把赢单商机与已激活合同交给计费系统的两个出站事件。 | @@ -31,7 +31,7 @@ Contract (Activated) Renewal task ──► Renewal opportunity ``` -1. **建立目录**——带编码、标价、计费类型的产品。 +1. **建立目录**——带编码、标价、SKU 的产品。 2. **构建报价**——使用目录中的明细行(参见 [报价](/zh-Hans/docs/sales/quotes))。 3. **接受时**系统会以报价的条款创建一份合同。 4. **合同被激活**——账户被提升为 *Customer*。 diff --git a/content/docs/revenue/index.zh-Hant.mdx b/content/docs/revenue/index.zh-Hant.mdx index e14de5193..f240d584a 100644 --- a/content/docs/revenue/index.zh-Hant.mdx +++ b/content/docs/revenue/index.zh-Hant.mdx @@ -11,7 +11,7 @@ description: 產品目錄、合約生命週期、續約,以及高價值折扣 | 領域 | 作用 | | --- | --- | -| [**產品**](/zh-Hant/docs/revenue/products) | 你所銷售內容的目錄——名稱、編碼、標價、計費類型。 | +| [**產品**](/zh-Hant/docs/revenue/products) | 你所銷售內容的目錄——名稱、編碼、標價、SKU。 | | [**合約**](/zh-Hant/docs/revenue/contracts) | 與客戶簽署的協議——條款、價值、續約。 | | [**審批**](/zh-Hant/docs/revenue/approvals) | 針對高價值折扣的多步驟審批。 | | [**計費交接**](/zh-Hant/docs/revenue/billing-handoff) | CRM 範圍的終點——把贏單商機與已啟用合約交給計費系統的兩個出站事件。 | @@ -31,7 +31,7 @@ Contract (Activated) Renewal task ──► Renewal opportunity ``` -1. **建立目錄**——帶編碼、標價、計費類型的產品。 +1. **建立目錄**——帶編碼、標價、SKU 的產品。 2. **構建報價**——使用目錄中的明細行(參見 [報價](/zh-Hant/docs/sales/quotes))。 3. **接受時**系統會以報價的條款建立一份合約。 4. **合約被啟用**——帳戶被提升為 *Customer*。 diff --git a/content/docs/revenue/products.mdx b/content/docs/revenue/products.mdx index 2e7b3fbd4..6d775e9b8 100644 --- a/content/docs/revenue/products.mdx +++ b/content/docs/revenue/products.mdx @@ -1,6 +1,6 @@ --- title: Products -description: The catalog of what you sell — codes, list prices, billing types and SKUs. +description: The catalog of what you sell — codes, list prices, SKUs and tax rates. --- # Products @@ -14,7 +14,7 @@ The detail screen has 3 sections: | Section | Fields | | --- | --- | | **Product Information** | Product code *(auto)*, name, description, category, family | -| **Pricing & Billing** | List price, cost, SKU, billing type, unit of measure, tax rate, taxable flag | +| **Pricing** | List price, cost, SKU, tax rate | | **Resources** | Product image, datasheet, product manager | ## Categories and families @@ -38,48 +38,19 @@ Categorise products two ways: Categories and families let you slice reports by product mix — *"how much SMB-tier subscription revenue did we book this quarter?"* -## Billing types - -| Billing type | Use for | -| --- | --- | -| **One-Time** *(default)* | Perpetual licenses, one-off purchases | -| **Monthly** | Monthly subscriptions | -| **Quarterly** | Quarterly subscriptions | -| **Annual** | Annual subscriptions | -| **Usage** | Consumption-based (per API call, per GB, etc.) | - -The billing type drives how the quote calculates totals (one-time fee vs. recurring) and feeds into revenue recognition reporting. - -## Units of measure - -Specify how the product is sold: - -- **Each** *(default)* — one unit per quantity -- **License** — per software license -- **Seat** — per named user -- **Hour** — for services -- **Day** — for services -- **Month** — for time-bounded subscriptions - ## Pricing fields | Field | Purpose | | --- | --- | | **List Price** | Standard published price — what shows up on a quote by default | | **Cost** | Your cost — used for margin calculation | -| **Default Tax Rate %** | Auto-applied to quote line items | -| **Taxable** | If unchecked, no tax is added (zero-rated products) | +| **Default Tax Rate %** | The rate carried on the product record | The system warns you if Cost is greater than List Price (which would be a losing sale). -## Inventory tracking - -For physical products: - -- **Quantity on Hand** — current inventory. -- **Reorder Point** — when to reorder. +## No inventory, no billing schedule -These fields are optional — leave them empty for non-inventory items (software, services). +A product record is a catalog entry and nothing more. HotCRM holds **no stock figure** — no quantity on hand, no reorder point, and nothing decrements on a closed deal — and **no billing schedule or unit of measure**: how a subscription is billed lives in the contract and the quote, not on the catalog row. Those fields used to be declared here and were read by nothing, which made the catalog look like an inventory and billing system it never was. Stock belongs to your fulfilment system; see [Billing Handoff](/docs/revenue/billing-handoff). ## The product image and datasheet @@ -105,8 +76,6 @@ The product catalog is **public read-only** for the whole organisation. Sales re - **Active Products** — `is_active = true`. - **By Category** — grouped. - **By Family** — grouped. -- **Subscriptions** — `category = subscription OR billing_type IN [monthly, quarterly, annual]`. -- **Low Stock** — `quantity_on_hand < reorder_point` (for inventory products). ## Tips for product managers @@ -121,5 +90,5 @@ The product catalog is **public read-only** for the whole organisation. Sales re ## Tips for admins -- The category, family, billing type and unit-of-measure picklists can be extended. See [Customization › Extending Objects](/docs/customization/extending-objects). +- The category and family picklists can be extended. See [Customization › Extending Objects](/docs/customization/extending-objects). - A product can be linked to multiple opportunities and quotes — deactivation doesn't break historical records. diff --git a/content/docs/revenue/products.zh-Hans.mdx b/content/docs/revenue/products.zh-Hans.mdx index e275c6574..15cff8136 100644 --- a/content/docs/revenue/products.zh-Hans.mdx +++ b/content/docs/revenue/products.zh-Hans.mdx @@ -1,6 +1,6 @@ --- title: 产品 -description: 你所销售内容的目录——编码、标价、计费类型和 SKU。 +description: 你所销售内容的目录——编码、标价、SKU 和税率。 --- # 产品 @@ -14,7 +14,7 @@ description: 你所销售内容的目录——编码、标价、计费类型和 | 区块 | 字段 | | --- | --- | | **Product Information** | 产品编码 *(自动)*、名称、描述、类别、产品系列 | -| **Pricing & Billing** | 标价、成本、SKU、计费类型、计量单位、税率、应税标记 | +| **Pricing** | 标价、成本、SKU、税率 | | **Resources** | 产品图片、规格表、产品经理 | ## 类别和产品系列 @@ -38,48 +38,19 @@ description: 你所销售内容的目录——编码、标价、计费类型和 类别和产品系列让你能按产品组合切分报表——*"本季度我们记入了多少 SMB 等级的订阅营收?"* -## 计费类型 - -| 计费类型 | 用于 | -| --- | --- | -| **One-Time** *(默认)* | 永久许可、一次性购买 | -| **Monthly** | 月度订阅 | -| **Quarterly** | 季度订阅 | -| **Annual** | 年度订阅 | -| **Usage** | 基于用量(按 API 调用、按 GB 等) | - -计费类型驱动报价如何计算总额(一次性费用 vs. 经常性),并输入到营收确认报表中。 - -## 计量单位 - -指定产品如何销售: - -- **Each** *(默认)*——每数量一个单位 -- **License**——按软件许可 -- **Seat**——按命名用户 -- **Hour**——用于服务 -- **Day**——用于服务 -- **Month**——用于有时限的订阅 - ## 定价字段 | 字段 | 用途 | | --- | --- | | **List Price** | 标准公布价——默认显示在报价上的价格 | | **Cost** | 你的成本——用于利润计算 | -| **Default Tax Rate %** | 自动应用到报价明细行 | -| **Taxable** | 如果未勾选,则不添加税(零税率产品) | +| **Default Tax Rate %** | 产品记录上携带的税率 | 如果成本大于标价(这会是一笔亏本的销售),系统会警告你。 -## 库存跟踪 - -针对物理产品: - -- **Quantity on Hand**——当前库存。 -- **Reorder Point**——何时补货。 +## 没有库存,也没有计费周期 -这些字段是可选的——对于非库存项(软件、服务)保持为空。 +产品记录就是一条目录条目,仅此而已。HotCRM **不保存任何库存数字**——没有库存数量、没有补货阈值,赢单也不会扣减任何东西——也**没有计费周期或计量单位**:订阅如何计费属于合同和报价,而不属于目录行。这些字段过去声明在这里、却没有任何东西读取,让目录看起来像一套它从来不是的库存与计费系统。库存属于你的履约系统;参见 [计费交接](/zh-Hans/docs/revenue/billing-handoff)。 ## 产品图片和规格表 @@ -105,8 +76,6 @@ description: 你所销售内容的目录——编码、标价、计费类型和 - **Active Products**——`is_active = true`。 - **By Category**——分组。 - **By Family**——分组。 -- **Subscriptions**——`category = subscription OR billing_type IN [monthly, quarterly, annual]`。 -- **Low Stock**——`quantity_on_hand < reorder_point`(针对库存产品)。 ## 给产品经理的提示 @@ -121,5 +90,5 @@ description: 你所销售内容的目录——编码、标价、计费类型和 ## 给管理员的提示 -- 类别、产品系列、计费类型和计量单位选项列表都可以扩展。参见 [定制 › 扩展对象](/zh-Hans/docs/customization/extending-objects)。 +- 类别和产品系列选项列表都可以扩展。参见 [定制 › 扩展对象](/zh-Hans/docs/customization/extending-objects)。 - 一个产品可以关联到多个商机和报价——停用不会破坏历史记录。 diff --git a/content/docs/revenue/products.zh-Hant.mdx b/content/docs/revenue/products.zh-Hant.mdx index 8356bd28d..7f84669ce 100644 --- a/content/docs/revenue/products.zh-Hant.mdx +++ b/content/docs/revenue/products.zh-Hant.mdx @@ -1,6 +1,6 @@ --- title: 產品 -description: 你所銷售內容的目錄——編碼、標價、計費類型和 SKU。 +description: 你所銷售內容的目錄——編碼、標價、SKU 和稅率。 --- # 產品 @@ -14,7 +14,7 @@ description: 你所銷售內容的目錄——編碼、標價、計費類型和 | 區塊 | 欄位 | | --- | --- | | **Product Information** | 產品編碼 *(自動)*、名稱、描述、類別、產品系列 | -| **Pricing & Billing** | 標價、成本、SKU、計費類型、計量單位、稅率、應稅標記 | +| **Pricing** | 標價、成本、SKU、稅率 | | **Resources** | 產品圖片、規格表、產品經理 | ## 類別和產品系列 @@ -38,48 +38,19 @@ description: 你所銷售內容的目錄——編碼、標價、計費類型和 類別和產品系列讓你能按產品組合切分報表——*"本季度我們記入了多少 SMB 等級的訂閱營收?"* -## 計費類型 - -| 計費類型 | 用於 | -| --- | --- | -| **One-Time** *(預設)* | 永久許可、一次性購買 | -| **Monthly** | 月度訂閱 | -| **Quarterly** | 季度訂閱 | -| **Annual** | 年度訂閱 | -| **Usage** | 基於用量(按 API 呼叫、按 GB 等) | - -計費類型驅動報價如何計算總額(一次性費用 vs. 經常性),並輸入到營收確認報表中。 - -## 計量單位 - -指定產品如何銷售: - -- **Each** *(預設)*——每數量一個單位 -- **License**——按軟體許可 -- **Seat**——按命名使用者 -- **Hour**——用於服務 -- **Day**——用於服務 -- **Month**——用於有時限的訂閱 - ## 定價欄位 | 欄位 | 用途 | | --- | --- | | **List Price** | 標準公佈價——預設顯示在報價上的價格 | | **Cost** | 你的成本——用於利潤計算 | -| **Default Tax Rate %** | 自動應用到報價明細行 | -| **Taxable** | 如果未勾選,則不新增稅(零稅率產品) | +| **Default Tax Rate %** | 產品記錄上攜帶的稅率 | 如果成本大於標價(這會是一筆虧本的銷售),系統會警告你。 -## 庫存追蹤 - -針對物理產品: - -- **Quantity on Hand**——當前庫存。 -- **Reorder Point**——何時補貨。 +## 沒有庫存,也沒有計費週期 -這些欄位是可選的——對於非庫存項(軟體、服務)保持為空。 +產品記錄就是一條目錄條目,僅此而已。HotCRM **不保存任何庫存數字**——沒有庫存數量、沒有補貨閾值,贏單也不會扣減任何東西——也**沒有計費週期或計量單位**:訂閱如何計費屬於合約和報價,而不屬於目錄列。這些欄位過去宣告在這裡、卻沒有任何東西讀取,讓目錄看起來像一套它從來不是的庫存與計費系統。庫存屬於你的履約系統;參見 [計費交接](/zh-Hant/docs/revenue/billing-handoff)。 ## 產品圖片和規格表 @@ -105,8 +76,6 @@ description: 你所銷售內容的目錄——編碼、標價、計費類型和 - **Active Products**——`is_active = true`。 - **By Category**——分組。 - **By Family**——分組。 -- **Subscriptions**——`category = subscription OR billing_type IN [monthly, quarterly, annual]`。 -- **Low Stock**——`quantity_on_hand < reorder_point`(針對庫存產品)。 ## 給產品經理的提示 @@ -121,5 +90,5 @@ description: 你所銷售內容的目錄——編碼、標價、計費類型和 ## 給管理員的提示 -- 類別、產品系列、計費類型和計量單位選項列表都可以擴展。參見 [定制 › 擴展物件](/zh-Hant/docs/customization/extending-objects)。 +- 類別和產品系列選項列表都可以擴展。參見 [定制 › 擴展物件](/zh-Hant/docs/customization/extending-objects)。 - 一個產品可以關聯到多個商機和報價——停用不會破壞歷史記錄。 diff --git a/content/docs/sales/accounts.mdx b/content/docs/sales/accounts.mdx index bc9d1f6ca..6f3eeb530 100644 --- a/content/docs/sales/accounts.mdx +++ b/content/docs/sales/accounts.mdx @@ -32,7 +32,7 @@ The account detail screen is organised into 6 sections: | Section | Fields | | --- | --- | | **Basic Information** | Account number, name, type, industry, description | -| **Financials** | Annual revenue, number of employees | +| **Financials** | Annual revenue, number of employees, child account revenue | | **Contact Information** | Phone, website, billing address, office location (with map) | | **Ownership & Status** | Owner, parent account, active flag | | **Branding** | Brand color (used as the avatar accent) | @@ -53,10 +53,11 @@ Acme Corp Global The hierarchy gives you: -- **Roll-up reports** — annual revenue of the global parent sums all children. -- **Cascading sharing** — when an admin grants access to the parent, all children inherit it. +- **Rolled-up revenue** — **Child Account Revenue**, on the *Financials* section, is the sum of the **Annual Revenue** of this account's **direct** children. The platform maintains it: it moves when a child's revenue is edited, when a child is re-parented onto another account, and when a child is deleted. It is one level deep, so a grandparent shows the total of its own children rather than of the whole tree. - **Easier navigation** — from a child you can jump to the global parent. +Two things the hierarchy deliberately does **not** do. It does **not** cascade sharing: accounts are private, and granting someone the parent gives them nothing on the children — use the sharing rules on [Administration › Sharing](/docs/administration/sharing-and-security) for that. And it does **not** roll anything else up: opportunities, cases and contacts belong to the account they name, and no report sums them up the tree. + ## Built-in rules - The account **name must be unique within your organization**. You'll get an error if you try to create a second account with a name you already use. Another organization on the same platform is free to have its own "Acme Corp" — the rule is scoped to your data, not the whole platform. The uniqueness check compares names exactly as typed, so `Acme Corp` and `ACME Corp` can both exist as separate accounts. Lead conversion is more forgiving: it ignores capitalisation and extra spaces when looking for an account to reuse (see [Leads](./leads)), so converting a lead for `ACME Corp` attaches it to your existing `Acme Corp` rather than creating a near-duplicate. diff --git a/content/docs/sales/accounts.zh-Hans.mdx b/content/docs/sales/accounts.zh-Hans.mdx index cf3629991..a8097b8fb 100644 --- a/content/docs/sales/accounts.zh-Hans.mdx +++ b/content/docs/sales/accounts.zh-Hans.mdx @@ -32,7 +32,7 @@ description: 你与之开展业务的公司——你的潜在客户、客户和 | 区块 | 字段 | | --- | --- | | **基本信息** | 客户编号、名称、类型、行业、描述 | -| **财务** | 年营收、员工人数 | +| **财务** | 年营收、员工人数、子公司年营收合计 | | **联系信息** | 电话、网站、账单地址、办公地点(含地图) | | **归属与状态** | 负责人、父级客户、活跃标志 | | **品牌** | 品牌颜色(用作头像强调色) | @@ -53,10 +53,11 @@ Acme Corp Global 该层级给你带来: -- **汇总报告**——全球父级的年营收会汇总所有子级。 -- **级联共享**——当管理员授予对父级的访问权限时,所有子级都会继承它。 +- **汇总营收**——*财务*区块上的**子公司年营收合计**,是本客户**直属**子公司**年营收**之和。它由平台维护:子公司营收被修改、子公司被改挂到另一个父级、或子公司被删除时,这个数字都会随之变动。它只汇总一层,因此祖父级看到的是它自己直属子级的合计,而不是整棵树的合计。 - **更便捷的导航**——从子级你可以跳转到全球父级。 +该层级刻意**不**做两件事。它**不**级联共享:客户是私有的,把父级授权给某人并不会带来任何子级的权限——需要这种效果请使用 [管理 › 共享](/zh-Hans/docs/administration/sharing-and-security) 上的共享规则。它也**不**汇总其它任何东西:商机、工单和联系人属于它们各自指向的那个客户,没有任何报表会沿着这棵树向上求和。 + ## 内置规则 - 客户**名称在你所在的组织内必须唯一**。如果你尝试用一个已存在的名称再建一个客户,会收到错误。同一平台上的其他组织可以有自己的 "Acme Corp" —— 这条规则只作用于你自己的数据,而不是整个平台。唯一性校验按原样精确比较名称,因此 `Acme Corp` 与 `ACME Corp` 可以作为两个独立的客户同时存在。线索转化则更宽松:它在寻找可复用的客户时会忽略大小写和多余空格(见[线索](./leads)),因此转化公司名为 `ACME Corp` 的线索时,会挂到你已有的 `Acme Corp` 上,而不是新建一个近似重复的客户。 diff --git a/content/docs/sales/accounts.zh-Hant.mdx b/content/docs/sales/accounts.zh-Hant.mdx index 14fbe0085..e216b3dd2 100644 --- a/content/docs/sales/accounts.zh-Hant.mdx +++ b/content/docs/sales/accounts.zh-Hant.mdx @@ -32,7 +32,7 @@ description: 你與之開展業務的公司——你的潛在客戶、客戶和 | 區塊 | 欄位 | | --- | --- | | **基本資訊** | 客戶編號、名稱、類型、產業、描述 | -| **財務** | 年營收、員工人數 | +| **財務** | 年營收、員工人數、子公司年營收合計 | | **聯絡資訊** | 電話、網站、帳單地址、辦公地點(含地圖) | | **歸屬與狀態** | 負責人、父級客戶、活躍標誌 | | **品牌** | 品牌顏色(用作頭像強調色) | @@ -53,10 +53,11 @@ Acme Corp Global 該層級給你帶來: -- **彙總報告**——全球父級的年營收會彙總所有子級。 -- **級聯共享**——當管理員授予對父級的存取權限時,所有子級都會繼承它。 +- **彙總營收**——*財務*區塊上的**子公司年營收合計**,是本客戶**直屬**子公司**年營收**之和。它由平台維護:子公司營收被修改、子公司被改掛到另一個父級、或子公司被刪除時,這個數字都會隨之變動。它只彙總一層,因此祖父級看到的是它自己直屬子級的合計,而不是整棵樹的合計。 - **更便捷的導覽**——從子級你可以跳轉到全球父級。 +該層級刻意**不**做兩件事。它**不**級聯共享:客戶是私有的,把父級授權給某人並不會帶來任何子級的權限——需要這種效果請使用 [管理 › 共享](/zh-Hant/docs/administration/sharing-and-security) 上的共享規則。它也**不**彙總其它任何東西:商機、工單和聯絡人屬於它們各自指向的那個客戶,沒有任何報表會沿著這棵樹向上求和。 + ## 內建規則 - 客戶**名稱在你所屬的組織內必須唯一**。如果你嘗試用一個已存在的名稱再建一個客戶,會收到錯誤。同一平台上的其他組織可以有自己的 "Acme Corp" —— 這條規則只作用於你自己的資料,而不是整個平台。唯一性檢查按原樣精確比較名稱,因此 `Acme Corp` 與 `ACME Corp` 可以作為兩個獨立的客戶同時存在。潛在客戶轉化則更寬鬆:它在尋找可重用的客戶時會忽略大小寫與多餘空格(見[潛在客戶](./leads)),因此轉化公司名為 `ACME Corp` 的潛在客戶時,會掛到你既有的 `Acme Corp` 上,而不是新建一個近似重複的客戶。 diff --git a/content/docs/sales/activities.mdx b/content/docs/sales/activities.mdx index 45e28e784..24675c72d 100644 --- a/content/docs/sales/activities.mdx +++ b/content/docs/sales/activities.mdx @@ -60,7 +60,7 @@ When you create an Urgent task, the owner gets an email alert. | **Assignment** | Owner | | **Related To** | Account, contact, opportunity, lead or case | | **Recurrence** | Is it recurring? Daily / weekly / monthly / yearly with an interval and end date | -| **Tracking** | Estimated hours, actual hours, progress % | +| **Tracking** | Progress % | ## Recurring tasks diff --git a/content/docs/sales/activities.zh-Hans.mdx b/content/docs/sales/activities.zh-Hans.mdx index 5b4c357f4..885ce927d 100644 --- a/content/docs/sales/activities.zh-Hans.mdx +++ b/content/docs/sales/activities.zh-Hans.mdx @@ -60,7 +60,7 @@ description: 任务——你仍然欠客户的事,挂在任意记录上。每 | **分配** | 负责人 | | **关联到** | 客户、联系人、商机、线索或案例 | | **重复** | 它是重复性的吗?每日 / 每周 / 每月 / 每年,带间隔和结束日期 | -| **跟踪** | 预计工时、实际工时、进度 % | +| **跟踪** | 进度 % | ## 重复任务 diff --git a/content/docs/sales/activities.zh-Hant.mdx b/content/docs/sales/activities.zh-Hant.mdx index 709a6f630..b29c16854 100644 --- a/content/docs/sales/activities.zh-Hant.mdx +++ b/content/docs/sales/activities.zh-Hant.mdx @@ -60,7 +60,7 @@ description: 任務——你仍然欠客戶的事,掛在任意記錄上。每 | **指派** | 負責人 | | **關聯到** | 客戶、聯絡人、商機、潛在客戶或案例 | | **重複** | 它是重複性的嗎?每日 / 每週 / 每月 / 每年,帶間隔和結束日期 | -| **追蹤** | 預計工時、實際工時、進度 % | +| **追蹤** | 進度 % | ## 重複任務 diff --git a/content/docs/sales/contacts.mdx b/content/docs/sales/contacts.mdx index f9a53b867..675bbc58b 100644 --- a/content/docs/sales/contacts.mdx +++ b/content/docs/sales/contacts.mdx @@ -14,10 +14,10 @@ The contact detail screen has 6 collapsible sections: | Section | Fields | | --- | --- | | **Identity** | Salutation, first name, last name, full name, profile picture | -| **Account & Role** | Contact owner, account, job title, department, reports-to | +| **Account & Role** | Contact owner, account, job title, department | | **Contact Information** | Email, phone, mobile | | **Mailing Address** | Mailing street, city, state/province, postal code and country — five separate fields | -| **Additional Info** | Birthday, lead source, description, last contacted | +| **Additional Info** | Lead source, description, last contacted | | **Communication Preferences** | Primary contact flag, do-not-call, email opt-out | ## The primary contact @@ -32,18 +32,11 @@ The primary contact is auto-suggested as the default on: This saves a lot of clicks. -## Org chart at the account +## No org chart — and what to use instead -Use **Reports To** to model the org structure inside an account: +There is **no reporting line on a contact**. A `Reports To` field was declared for one, and the tree it promised was never built: no page rendered it, no report grouped by it, and the Copilot never read it when summarising an account. Rather than keep a field whose documented behaviour did not exist, HotCRM records seniority where something acts on it — **job title** and **department**, which reports do group by and the Copilot does read. -``` -CEO -├── VP Sales -│ └── Sales Director -└── VP Engineering -``` - -The account detail page renders this as a clickable tree, so you can see at a glance who reports to whom. The AI Copilot uses it when summarising the account — it can tell you which people are decision-makers vs. influencers. +Nor is a date of birth stored. `Birthdate` was importable and read by nothing; personal data with no purpose is a liability rather than a feature, so it is gone too. ## Communication preferences (GDPR / CCPA) @@ -93,7 +86,7 @@ So *"Draft a follow-up to Jane"* gives you a personalised, on-brand email in sec ## Tips for sales reps - ✅ Always set a job title and department — it's used by reports and by the Copilot. -- ✅ Use the Reports To field — it makes account meetings much easier to prep for. +- ✅ Fill in job title and department for every contact you meet — they are what the Copilot and the reports actually read. - ✅ Mark exactly one primary contact per account. ## Tips for admins diff --git a/content/docs/sales/contacts.zh-Hans.mdx b/content/docs/sales/contacts.zh-Hans.mdx index cdd958271..be5f5ddda 100644 --- a/content/docs/sales/contacts.zh-Hans.mdx +++ b/content/docs/sales/contacts.zh-Hans.mdx @@ -14,10 +14,10 @@ description: 你向其销售并为其提供服务的客户处的人——决策 | 区块 | 字段 | | --- | --- | | **身份信息** | 称谓、名、姓、全名、头像 | -| **客户与职务** | 联系人负责人、所属客户、职位、部门、直属上级 | +| **客户与职务** | 联系人负责人、所属客户、职位、部门 | | **联系方式** | 邮箱、电话、手机 | | **邮寄地址** | 邮寄地址、邮寄城市、邮寄省份、邮政编码、邮寄国家(五个独立字段) | -| **附加信息** | 生日、线索来源、描述、最近联系时间 | +| **附加信息** | 线索来源、描述、最近联系时间 | | **沟通偏好** | 主要联系人、禁止致电、拒绝邮件 | ## 主要联系人 @@ -32,18 +32,11 @@ description: 你向其销售并为其提供服务的客户处的人——决策 这能省去大量点击。 -## 客户处的组织架构图 +## 没有组织架构图——以及改用什么 -使用**直属上级**来建模客户内部的组织结构: +联系人上**没有汇报关系字段**。曾经为此声明过一个 `Reports To` 字段,而它承诺的那棵树从未被构建:没有页面渲染它,没有报表按它分组,Copilot 在总结客户时也从不读取它。与其保留一个文档行为并不存在的字段,HotCRM 把资历信息记录在真正有东西读取的地方——**职位**与**部门**,报表确实按它们分组,Copilot 也确实读取它们。 -``` -CEO -├── VP Sales -│ └── Sales Director -└── VP Engineering -``` - -客户详情页将其渲染为一棵可点击的树,因此你能一眼看出谁向谁汇报。AI Copilot 在总结客户时会用到它——它能告诉你哪些人是决策者、哪些是影响者。 +也不再存储出生日期。`Birthdate` 可以导入,却没有任何东西读取;没有用途的个人数据是负债而不是功能,因此它也一并移除。 ## 沟通偏好(GDPR / CCPA) @@ -93,7 +86,7 @@ CEO ## 给销售代表的提示 - ✅ 始终设置职位和部门——它会被报告和 Copilot 使用。 -- ✅ 使用直属上级字段——它让客户会议的准备工作容易得多。 +- ✅ 为你见过的每一位联系人填好职位与部门——它们才是 Copilot 和报表真正读取的字段。 - ✅ 每个客户恰好标记一个主要联系人。 ## 给管理员的提示 diff --git a/content/docs/sales/contacts.zh-Hant.mdx b/content/docs/sales/contacts.zh-Hant.mdx index 668226d75..7e0b9fc41 100644 --- a/content/docs/sales/contacts.zh-Hant.mdx +++ b/content/docs/sales/contacts.zh-Hant.mdx @@ -14,10 +14,10 @@ description: 你向其銷售並為其提供服務的客戶處的人——決策 | 區塊 | 欄位 | | --- | --- | | **身份資訊** | 稱謂、名、姓、全名、頭像 | -| **客戶與職務** | 聯絡人負責人、所屬客戶、職位、部門、直屬上級 | +| **客戶與職務** | 聯絡人負責人、所屬客戶、職位、部門 | | **聯絡方式** | 郵箱、電話、手機 | | **郵寄地址** | 郵寄地址、郵寄城市、郵寄省份、郵政編碼、郵寄國家(五個獨立欄位) | -| **附加資訊** | 生日、線索來源、描述、最近聯絡時間 | +| **附加資訊** | 線索來源、描述、最近聯絡時間 | | **溝通偏好** | 主要聯絡人、禁止致電、拒絕郵件 | ## 主要聯絡人 @@ -32,18 +32,11 @@ description: 你向其銷售並為其提供服務的客戶處的人——決策 這能省去大量點擊。 -## 客戶處的組織架構圖 +## 沒有組織架構圖——以及改用什麼 -使用**直屬上級**來建模客戶內部的組織結構: +聯絡人上**沒有匯報關係欄位**。曾經為此宣告過一個 `Reports To` 欄位,而它承諾的那棵樹從未被建構:沒有頁面渲染它,沒有報表按它分組,Copilot 在總結客戶時也從不讀取它。與其保留一個文件行為並不存在的欄位,HotCRM 把資歷資訊記錄在真正有東西讀取的地方——**職位**與**部門**,報表確實按它們分組,Copilot 也確實讀取它們。 -``` -CEO -├── VP Sales -│ └── Sales Director -└── VP Engineering -``` - -客戶詳情頁將其渲染為一棵可點擊的樹,因此你能一眼看出誰向誰匯報。AI Copilot 在總結客戶時會用到它——它能告訴你哪些人是決策者、哪些是影響者。 +也不再儲存出生日期。`Birthdate` 可以匯入,卻沒有任何東西讀取;沒有用途的個人資料是負債而不是功能,因此它也一併移除。 ## 溝通偏好(GDPR / CCPA) @@ -93,7 +86,7 @@ CEO ## 給銷售代表的提示 - ✅ 始終設定職位和部門——它會被報告和 Copilot 使用。 -- ✅ 使用直屬上級欄位——它讓客戶會議的準備工作容易得多。 +- ✅ 為你見過的每一位聯絡人填好職位與部門——它們才是 Copilot 和報表真正讀取的欄位。 - ✅ 每個客戶恰好標記一個主要聯絡人。 ## 給管理員的提示 diff --git a/content/docs/service/cases.mdx b/content/docs/service/cases.mdx index 4e1c37861..209563bdd 100644 --- a/content/docs/service/cases.mdx +++ b/content/docs/service/cases.mdx @@ -52,7 +52,7 @@ This drives reporting (which channels generate the most volume) and is auto-fill ## What a case record stores -`crm_case` declares **28 fields** (`src/objects/case.object.ts`), and three different things organise them: the object's own field groups, the case detail screen, and the case form. Only the first of the three has six sections, and no two of them agree — so it is worth knowing which one you are being shown. +`crm_case` declares **27 fields** (`src/objects/case.object.ts`), and three different things organise them: the object's own field groups, the case detail screen, and the case form. Only the first of the three has six sections, and no two of them agree — so it is worth knowing which one you are being shown. ### The object's field groups @@ -60,18 +60,18 @@ The six names below are real, but not one of them is a screen: they are `crm_cas | Field group | Fields | | --- | --- | -| **Case Information** | Case Number, Subject, Display Title, Description, Account, Contact, Status, Case Type, Parent Case | +| **Case Information** | Case Number, Subject, Display Title, Description, Account, Contact, Status, Case Type | | **Origin & Routing** | Case Owner, Case Origin | | **SLA & Priority** | Priority, Created Date, Closed Date, First Response Date, Resolution Time (Hours), SLA Due Date, SLA Violated | -| **Resolution** | Resolution, Customer Satisfaction, Customer Feedback, Customer Signature | +| **Resolution** | Resolution, Resolved by Article, Customer Satisfaction, Customer Feedback | | **Escalation** *(collapsed by default)* | Escalated, Escalated Date, Escalation Reason | | **System** *(collapsed by default)* | Internal Notes, Is Closed | -Five of those six rows used to be written differently on this page, and the differences were not cosmetic: **Parent Case** is in *Case Information*, not *Origin & Routing*; **Status** and **Case Type** are in *Case Information*, not *SLA & Priority*; **Closed Date** is in *SLA & Priority*, not *Resolution*; and *System* holds **Internal Notes** and **Is Closed** — there is no *audit trail* field on a case at all, and the timestamps sit in *SLA & Priority*. Only *Escalation* was right as it stood. **Priority Rank**, the hidden sort key that orders *My Open Cases*, is in no group at all. +Five of those six rows used to be written differently on this page, and the differences were not cosmetic: **Status** and **Case Type** are in *Case Information*, not *SLA & Priority*; **Closed Date** is in *SLA & Priority*, not *Resolution*; and *System* holds **Internal Notes** and **Is Closed** — there is no *audit trail* field on a case at all, and the timestamps sit in *SLA & Priority*. Only *Escalation* was right as it stood. **Priority Rank**, the hidden sort key that orders *My Open Cases*, is in no group at all. ### The detail screen -The **Details** tab of the case detail page (`src/pages/case_detail.page.ts`) declares **three** sections, holding **16** of the 28 fields: +The **Details** tab of the case detail page (`src/pages/case_detail.page.ts`) declares **three** sections, holding **16** of the 27 fields: | Section | Fields | | --- | --- | @@ -83,15 +83,15 @@ The **Details** tab of the case detail page (`src/pages/case_detail.page.ts`) de ### The form -The case form (`src/views/case.view.ts`) is tabbed, and has three sections of its own — different names, different contents, 24 of the 28 fields: +The case form (`src/views/case.view.ts`) is tabbed, and has three sections of its own — different names, different contents, 22 of the 27 fields: | Form tab | Fields | | --- | --- | | **Case** | Case Number, Subject, Account, Contact, Status, Priority, Case Origin, Case Owner, Description | -| **SLA** | Created Date, First Response Date, SLA Due Date, Resolution Time (Hours), SLA Violated, Escalated, Escalation Reason, Parent Case | -| **Resolution** | Resolution, Internal Notes, Customer Satisfaction, Customer Feedback, Customer Signature, Closed Date, Is Closed | +| **SLA** | Created Date, First Response Date, SLA Due Date, Resolution Time (Hours), SLA Violated, Escalated, Escalation Reason | +| **Resolution** | Resolution, Internal Notes, Customer Satisfaction, Customer Feedback, Closed Date, Is Closed | -The two are not one screen wearing different chrome. **Parent Case** is on the form and nowhere else — the detail screen carries it in no section — while **Case Type** is the other way round. The public support form at `/forms/support` is a third layout again: a single section with four fields (subject, description, type, priority). +The two are not one screen wearing different chrome. **Case Type** is on the detail screen and nowhere else — the form carries it in no tab. The public support form at `/forms/support` is a third layout again: a single section with four fields (subject, description, type, priority). ## What happens automatically @@ -176,7 +176,7 @@ When you open a case, you'll see: - **Status path** — a Salesforce-style horizontal flow showing New → In Progress → Waiting on Customer → Escalated → Resolved → Closed, so an agent can see at a glance where the case stands and what the next stop is. - **Customer panel** — there is no such panel. The page (`src/pages/case_detail.page.ts`) declares two regions and nothing else: a header holding the three components above, and a main region holding the tab strip below. What customer context there is sits in those: the **account** appears three times over — the header subtitle, the Key Information strip, and a field in the *Details* tab — and the **primary contact** is a field in that same tab's *Case Information* section. **Contract tier** and **open cases this month** are on no case surface at all. A case links to no contract, and `crm_contract` carries no tier field either; the nearest real field is **Customer Tier** on the account record, which this page does not show. Nothing in this app counts a customer's cases by month. - **Three tabs:** - - **Details** — the case description and resolution in full, but not "all metadata fields": **16** of the object's **28**, in the three sections listed under *What a case record stores* above. The twelve that are not on the tab are **First Response Date**, **Created Date**, **Closed Date**, **Escalated Date**, **Is Closed**, **Parent Case**, **Customer Satisfaction**, **Customer Feedback**, **Customer Signature**, **Internal Notes**, **Priority Rank** and **Display Title**. Nine of them are on the case form instead; the remaining three are on no case screen at all — **Escalated Date** is written by the escalation flows and displayed nowhere, **Priority Rank** is the sort key behind *My Open Cases*, and **Display Title** is the object's `nameField`, the title formula that names a case wherever one is referenced rather than a field you read here. **First Response Date** is the omission worth knowing about: *What happens automatically* above explains at length how it gets stamped, and this is not the tab you can read it on. + - **Details** — the case description and resolution in full, but not "all metadata fields": **16** of the object's **27**, in the three sections listed under *What a case record stores* above. The eleven that are not on the tab are **First Response Date**, **Created Date**, **Closed Date**, **Escalated Date**, **Is Closed**, **Customer Satisfaction**, **Customer Feedback**, **Internal Notes**, **Priority Rank**, **Display Title** and **Resolved by Article**. Seven of them are on the case form instead; the remaining four are on no case screen at all — **Escalated Date** is written by the escalation flows and displayed nowhere, **Priority Rank** is the sort key behind *My Open Cases*, **Display Title** is the object's `nameField`, the title formula that names a case wherever one is referenced rather than a field you read here, and **Resolved by Article** is read by the deflection measures rather than by a screen. **First Response Date** is the omission worth knowing about: *What happens automatically* above explains at length how it gets stamped, and this is not the tab you can read it on. - **Related** — one list, not four: **Open Tasks** — the `crm_task` records pointing at this case through **Related Case** (`related_to_case`), filtered to those whose status is not *Completed*, ten at a time. That is the whole tab. There is no attachments list: files can be attached to a case (the object enables them), but no component on this page lists them. There is no linked-opportunity list, because `crm_case` has no opportunity relationship in either direction. And there is no milestone list — the case's three milestones, *escalated*, *resolved* and *closed*, are emitted as entries in the **Activity** timeline, not as records here. - **Activity** — a unified timeline (more on this below). - **AI Reference Rail** — there is no rail on this page, of any kind. The main region holds the tab strip and nothing else, and not one component on the page is AI-driven. The service skills are real — see *How the AI assistant helps* below — but you reach them by asking, not from a panel beside the case. The one reference rail this app does render sits on the opportunity detail page, and even there it lists related records (quotes, products, open tasks) rather than suggestions. diff --git a/content/docs/service/cases.zh-Hans.mdx b/content/docs/service/cases.zh-Hans.mdx index 94d83fc5c..ecdeb4886 100644 --- a/content/docs/service/cases.zh-Hans.mdx +++ b/content/docs/service/cases.zh-Hans.mdx @@ -52,7 +52,7 @@ description: 客户支持工单——主题、优先级、状态、解决方案 ## 工单记录存储的内容 -`crm_case` 一共声明 **28 个字段**(`src/objects/case.object.ts`),而把它们组织起来的东西有三样:对象自己的字段分组、工单详情界面、工单表单。三者里只有第一样是六个分区,而且两两都不一致——所以要紧的是分清你看的是哪一样。 +`crm_case` 一共声明 **27 个字段**(`src/objects/case.object.ts`),而把它们组织起来的东西有三样:对象自己的字段分组、工单详情界面、工单表单。三者里只有第一样是六个分区,而且两两都不一致——所以要紧的是分清你看的是哪一样。 ### 对象自己的字段分组 @@ -60,18 +60,18 @@ description: 客户支持工单——主题、优先级、状态、解决方案 | 字段分组 | 字段 | | --- | --- | -| **Case Information** | 工单编号、主题、显示名称、描述、账户、联系人、状态、类型、父工单 | +| **Case Information** | 工单编号、主题、显示名称、描述、账户、联系人、状态、类型 | | **Origin & Routing** | 负责人、来源 | | **SLA & Priority** | 优先级、创建日期、关闭日期、首次响应日期、解决时间(小时)、SLA 截止日期、SLA Violated | -| **Resolution** | 解决方案、客户满意度、客户反馈、客户签名 | +| **Resolution** | 解决方案、解决所用文章、客户满意度、客户反馈 | | **Escalation** *(默认折叠)* | 升级标记、升级日期、升级原因 | | **System** *(默认折叠)* | 内部备注、关闭标记 | -这六行里有五行,本页原先写的是另一个样子,而且差的不是细枝末节:**父工单**在 *Case Information* 里,不在 *Origin & Routing*;**状态**与**类型**也在 *Case Information* 里,不在 *SLA & Priority*;**关闭日期**在 *SLA & Priority* 里,不在 *Resolution*;*System* 装的是**内部备注**与**关闭标记**——工单上根本没有名为「审计跟踪」的字段,时间戳则待在 *SLA & Priority* 里。六行里只有 *Escalation* 原样就是对的。**优先级排序**——把 *My Open Cases* 排出先后的那个隐藏排序键——则不属于任何一个分组。 +这六行里有五行,本页原先写的是另一个样子,而且差的不是细枝末节:**状态**与**类型**在 *Case Information* 里,不在 *SLA & Priority*;**关闭日期**在 *SLA & Priority* 里,不在 *Resolution*;*System* 装的是**内部备注**与**关闭标记**——工单上根本没有名为「审计跟踪」的字段,时间戳则待在 *SLA & Priority* 里。六行里只有 *Escalation* 原样就是对的。**优先级排序**——把 *My Open Cases* 排出先后的那个隐藏排序键——则不属于任何一个分组。 ### 详情界面 -工单详情页(`src/pages/case_detail.page.ts`)的 **Details** 页签声明了**三个**分区,装着 28 个字段中的 **16** 个: +工单详情页(`src/pages/case_detail.page.ts`)的 **Details** 页签声明了**三个**分区,装着 27 个字段中的 **16** 个: | 分区 | 字段 | | --- | --- | @@ -83,15 +83,15 @@ description: 客户支持工单——主题、优先级、状态、解决方案 ### 表单 -工单表单(`src/views/case.view.ts`)是分页签的,自己另有三个分区——名字不同、内容也不同,覆盖 28 个字段中的 24 个: +工单表单(`src/views/case.view.ts`)是分页签的,自己另有三个分区——名字不同、内容也不同,覆盖 27 个字段中的 22 个: | 表单页签 | 字段 | | --- | --- | | **Case** | 工单编号、主题、账户、联系人、状态、优先级、来源、负责人、描述 | -| **SLA** | 创建日期、首次响应日期、SLA 截止日期、解决时间(小时)、SLA Violated、升级标记、升级原因、父工单 | -| **Resolution** | 解决方案、内部备注、客户满意度、客户反馈、客户签名、关闭日期、关闭标记 | +| **SLA** | 创建日期、首次响应日期、SLA 截止日期、解决时间(小时)、SLA Violated、升级标记、升级原因 | +| **Resolution** | 解决方案、内部备注、客户满意度、客户反馈、关闭日期、关闭标记 | -这两者并不是同一个界面换了层皮。**父工单**只在表单上有——详情界面的任何一个分区里都没有它——**类型**则正好反过来。`/forms/support` 上那张公开支持表单又是第三种排布:一个分区、四个字段(主题、描述、类型、优先级)。 +这两者并不是同一个界面换了层皮。**类型**只在详情界面上有——表单的任何一个页签里都没有它。`/forms/support` 上那张公开支持表单又是第三种排布:一个分区、四个字段(主题、描述、类型、优先级)。 ## 自动发生的事 @@ -176,7 +176,7 @@ description: 客户支持工单——主题、优先级、状态、解决方案 - **状态路径**——一个 Salesforce 风格的水平流程,显示 New → In Progress → Waiting on Customer → Escalated → Resolved → Closed,让客服一眼就能看出工单的进展和下一站。 - **客户面板**——不存在这样一个面板。这一页(`src/pages/case_detail.page.ts`)只声明了两个区域,再无其他:一个页眉区域装着上面那三个组件,一个主区域装着下面那组页签。客户上下文就散落在这两处:**账户**在页面上出现了三次——页眉副标题、关键信息摘要栏,以及 *Details* 页签里的一个字段;**主要联系人**则是同一页签 *Case Information* 分区里的一个字段。**合同等级**与**本月未结工单**在工单的任何界面上都没有承载:工单不关联任何合同,`crm_contract` 上也没有等级字段;最接近的真实字段是账户记录上的 **Customer Tier**(中文界面作「客户分层」),而这一页并不展示它。本应用里也没有任何东西按月统计一个客户的工单数。 - **三个标签页:** - - **Details**——完整的工单描述和解决方案是有的,但不是「所有元数据字段」:是对象 **28** 个字段里的 **16** 个,分在上文《工单记录存储的内容》列出的那三个分区里。不在这个页签上的十二个是:**首次响应日期**、**创建日期**、**关闭日期**、**升级日期**、**关闭标记**、**父工单**、**客户满意度**、**客户反馈**、**客户签名**、**内部备注**、**优先级排序**、**显示名称**。其中九个在工单表单上;剩下三个在工单的任何界面上都没有——**升级日期**由升级流程写入,但哪里都不展示,**优先级排序**是 *My Open Cases* 背后的排序键,**显示名称**是对象的 `nameField`,一个在任何地方引用工单时给它命名的标题公式,而不是你在这里能读到的字段。这里面最该知道的缺席者是**首次响应日期**:上文《自动发生的事》整整一段讲清了它是怎么打戳的,而你读不到它的地方正是这个页签。 + - **Details**——完整的工单描述和解决方案是有的,但不是「所有元数据字段」:是对象 **27** 个字段里的 **16** 个,分在上文《工单记录存储的内容》列出的那三个分区里。不在这个页签上的十一个是:**首次响应日期**、**创建日期**、**关闭日期**、**升级日期**、**关闭标记**、**客户满意度**、**客户反馈**、**内部备注**、**优先级排序**、**显示名称**、**解决所用文章**。其中七个在工单表单上;剩下四个在工单的任何界面上都没有——**升级日期**由升级流程写入,但哪里都不展示,**优先级排序**是 *My Open Cases* 背后的排序键,**显示名称**是对象的 `nameField`,一个在任何地方引用工单时给它命名的标题公式,而不是你在这里能读到的字段,**解决所用文章**则是被知识库转移率指标读取,而不是被某个界面读取。这里面最该知道的缺席者是**首次响应日期**:上文《自动发生的事》整整一段讲清了它是怎么打戳的,而你读不到它的地方正是这个页签。 - **Related**——只有一个列表,不是四个:**Open Tasks**——通过**关联工单**(`related_to_case`)指向本工单的 `crm_task` 记录,过滤掉状态为 *Completed* 的,一次最多十条。整个页签就这些。没有附件列表:工单是可以挂附件的(对象上开了这项能力),但这一页没有任何组件把它们列出来。没有关联商机列表,因为 `crm_case` 与商机之间双向都不存在任何关系字段。也没有里程碑列表——工单的三个里程碑(*escalated*、*resolved*、*closed*)是以条目形式发到 **Activity** 时间线上的,不是在这里当记录列出来。 - **Activity**——统一的时间线(下文详述)。 - **AI 参考栏**——这一页上不存在任何形式的参考栏。主区域里除了那组页签什么都没有,页面上也没有任何一个组件是 AI 驱动的。服务技能本身是真实存在的——见下文《AI Copilot 如何提供帮助》一节——但你是靠提问去用它们,而不是靠工单旁边的一块面板。本应用里唯一真正渲染出来的参考栏在商机详情页上,而且即便在那里,它列的也是关联记录(报价、产品、未完成任务),不是什么建议。 diff --git a/content/docs/service/cases.zh-Hant.mdx b/content/docs/service/cases.zh-Hant.mdx index 0429e6c6c..47be71bc5 100644 --- a/content/docs/service/cases.zh-Hant.mdx +++ b/content/docs/service/cases.zh-Hant.mdx @@ -52,7 +52,7 @@ description: 客戶支援工單——主題、優先順序、狀態、解決方 ## 工單記錄儲存的內容 -`crm_case` 一共宣告 **28 個欄位**(`src/objects/case.object.ts`),而把它們組織起來的東西有三樣:物件自己的欄位分組、工單詳情介面、工單表單。三者裡只有第一樣是六個分區,而且兩兩都不一致——所以要緊的是分清你看的是哪一樣。 +`crm_case` 一共宣告 **27 個欄位**(`src/objects/case.object.ts`),而把它們組織起來的東西有三樣:物件自己的欄位分組、工單詳情介面、工單表單。三者裡只有第一樣是六個分區,而且兩兩都不一致——所以要緊的是分清你看的是哪一樣。 ### 物件自己的欄位分組 @@ -60,18 +60,18 @@ description: 客戶支援工單——主題、優先順序、狀態、解決方 | 欄位分組 | 欄位 | | --- | --- | -| **Case Information** | 工單編號、主題、顯示名稱、描述、帳戶、聯絡人、狀態、類型、父工單 | +| **Case Information** | 工單編號、主題、顯示名稱、描述、帳戶、聯絡人、狀態、類型 | | **Origin & Routing** | 負責人、來源 | | **SLA & Priority** | 優先順序、建立日期、關閉日期、首次回應日期、解決時間(小時)、SLA 截止日期、SLA Violated | -| **Resolution** | 解決方案、客戶滿意度、客戶回饋、客戶簽名 | +| **Resolution** | 解決方案、解決所用文章、客戶滿意度、客戶回饋 | | **Escalation** *(預設折疊)* | 升級標記、升級日期、升級原因 | | **System** *(預設折疊)* | 內部備註、關閉標記 | -這六列裡有五列,本頁原先寫的是另一個樣子,而且差的不是細枝末節:**父工單**在 *Case Information* 裡,不在 *Origin & Routing*;**狀態**與**類型**也在 *Case Information* 裡,不在 *SLA & Priority*;**關閉日期**在 *SLA & Priority* 裡,不在 *Resolution*;*System* 裝的是**內部備註**與**關閉標記**——工單上根本沒有名為「審計追蹤」的欄位,時間戳則待在 *SLA & Priority* 裡。六列裡只有 *Escalation* 原樣就是對的。**優先順序排序**——把 *My Open Cases* 排出先後的那個隱藏排序鍵——則不屬於任何一個分組。 +這六列裡有五列,本頁原先寫的是另一個樣子,而且差的不是細枝末節:**狀態**與**類型**在 *Case Information* 裡,不在 *SLA & Priority*;**關閉日期**在 *SLA & Priority* 裡,不在 *Resolution*;*System* 裝的是**內部備註**與**關閉標記**——工單上根本沒有名為「審計追蹤」的欄位,時間戳則待在 *SLA & Priority* 裡。六列裡只有 *Escalation* 原樣就是對的。**優先順序排序**——把 *My Open Cases* 排出先後的那個隱藏排序鍵——則不屬於任何一個分組。 ### 詳情介面 -工單詳情頁(`src/pages/case_detail.page.ts`)的 **Details** 頁籤宣告了**三個**分區,裝著 28 個欄位中的 **16** 個: +工單詳情頁(`src/pages/case_detail.page.ts`)的 **Details** 頁籤宣告了**三個**分區,裝著 27 個欄位中的 **16** 個: | 分區 | 欄位 | | --- | --- | @@ -83,15 +83,15 @@ description: 客戶支援工單——主題、優先順序、狀態、解決方 ### 表單 -工單表單(`src/views/case.view.ts`)是分頁籤的,自己另有三個分區——名字不同、內容也不同,涵蓋 28 個欄位中的 24 個: +工單表單(`src/views/case.view.ts`)是分頁籤的,自己另有三個分區——名字不同、內容也不同,涵蓋 27 個欄位中的 22 個: | 表單頁籤 | 欄位 | | --- | --- | | **Case** | 工單編號、主題、帳戶、聯絡人、狀態、優先順序、來源、負責人、描述 | -| **SLA** | 建立日期、首次回應日期、SLA 截止日期、解決時間(小時)、SLA Violated、升級標記、升級原因、父工單 | -| **Resolution** | 解決方案、內部備註、客戶滿意度、客戶回饋、客戶簽名、關閉日期、關閉標記 | +| **SLA** | 建立日期、首次回應日期、SLA 截止日期、解決時間(小時)、SLA Violated、升級標記、升級原因 | +| **Resolution** | 解決方案、內部備註、客戶滿意度、客戶回饋、關閉日期、關閉標記 | -這兩者並不是同一個介面換了層皮。**父工單**只在表單上有——詳情介面的任何一個分區裡都沒有它——**類型**則正好反過來。`/forms/support` 上那張公開支援表單又是第三種排佈:一個分區、四個欄位(主題、描述、類型、優先順序)。 +這兩者並不是同一個介面換了層皮。**類型**只在詳情介面上有——表單的任何一個頁籤裡都沒有它。`/forms/support` 上那張公開支援表單又是第三種排佈:一個分區、四個欄位(主題、描述、類型、優先順序)。 ## 自動發生的事 @@ -176,7 +176,7 @@ description: 客戶支援工單——主題、優先順序、狀態、解決方 - **狀態路徑**——一個 Salesforce 風格的水平流程,顯示 New → In Progress → Waiting on Customer → Escalated → Resolved → Closed,讓客服一眼就能看出工單的進展和下一站。 - **客戶面板**——不存在這樣一個面板。這一頁(`src/pages/case_detail.page.ts`)只宣告了兩個區域,再無其他:一個頁首區域裝著上面那三個元件,一個主區域裝著下面那組頁籤。客戶脈絡就散落在這兩處:**帳戶**在頁面上出現了三次——頁首副標題、關鍵資訊摘要欄,以及 *Details* 頁籤裡的一個欄位;**主要聯絡人**則是同一頁籤 *Case Information* 分區裡的一個欄位。**合約等級**與**本月未結工單**在工單的任何介面上都沒有承載:工單不關聯任何合約,`crm_contract` 上也沒有等級欄位;最接近的真實欄位是帳戶記錄上的 **Customer Tier**(中文介面作「客戶分層」),而這一頁並不展示它。本應用裡也沒有任何東西按月統計一個客戶的工單數。 - **三個標籤頁:** - - **Details**——完整的工單描述和解決方案是有的,但不是「所有元資料欄位」:是物件 **28** 個欄位裡的 **16** 個,分在上文《工單記錄儲存的內容》列出的那三個分區裡。不在這個頁籤上的十二個是:**首次回應日期**、**建立日期**、**關閉日期**、**升級日期**、**關閉標記**、**父工單**、**客戶滿意度**、**客戶回饋**、**客戶簽名**、**內部備註**、**優先順序排序**、**顯示名稱**。其中九個在工單表單上;剩下三個在工單的任何介面上都沒有——**升級日期**由升級流程寫入,但哪裡都不展示,**優先順序排序**是 *My Open Cases* 背後的排序鍵,**顯示名稱**是物件的 `nameField`,一個在任何地方引用工單時給它命名的標題公式,而不是你在這裡能讀到的欄位。這裡面最該知道的缺席者是**首次回應日期**:上文《自動發生的事》整整一段講清了它是怎麼打戳的,而你讀不到它的地方正是這個頁籤。 + - **Details**——完整的工單描述和解決方案是有的,但不是「所有元資料欄位」:是物件 **27** 個欄位裡的 **16** 個,分在上文《工單記錄儲存的內容》列出的那三個分區裡。不在這個頁籤上的十一個是:**首次回應日期**、**建立日期**、**關閉日期**、**升級日期**、**關閉標記**、**客戶滿意度**、**客戶回饋**、**內部備註**、**優先順序排序**、**顯示名稱**、**解決所用文章**。其中七個在工單表單上;剩下四個在工單的任何介面上都沒有——**升級日期**由升級流程寫入,但哪裡都不展示,**優先順序排序**是 *My Open Cases* 背後的排序鍵,**顯示名稱**是物件的 `nameField`,一個在任何地方引用工單時給它命名的標題公式,而不是你在這裡能讀到的欄位,**解決所用文章**則是被知識庫轉移率指標讀取,而不是被某個介面讀取。這裡面最該知道的缺席者是**首次回應日期**:上文《自動發生的事》整整一段講清了它是怎麼打戳的,而你讀不到它的地方正是這個頁籤。 - **Related**——只有一個列表,不是四個:**Open Tasks**——透過**關聯工單**(`related_to_case`)指向本工單的 `crm_task` 記錄,過濾掉狀態為 *Completed* 的,一次最多十筆。整個頁籤就這些。沒有附件列表:工單是可以掛附件的(物件上開了這項能力),但這一頁沒有任何元件把它們列出來。沒有關聯商機列表,因為 `crm_case` 與商機之間雙向都不存在任何關係欄位。也沒有里程碑列表——工單的三個里程碑(*escalated*、*resolved*、*closed*)是以條目形式發到 **Activity** 時間線上的,不是在這裡當記錄列出來。 - **Activity**——統一的時間線(下文詳述)。 - **AI 參考欄**——這一頁上不存在任何形式的參考欄。主區域裡除了那組頁籤什麼都沒有,頁面上也沒有任何一個元件是 AI 驅動的。服務技能本身是真實存在的——見下文《AI Copilot 如何提供幫助》一節——但你是靠提問去用它們,而不是靠工單旁邊的一塊面板。本應用裡唯一真正渲染出來的參考欄在商機詳情頁上,而且即便在那裡,它列的也是關聯記錄(報價、產品、未完成任務),不是什麼建議。 diff --git a/docs/STATUS.md b/docs/STATUS.md index b23e5857e..d99c3662d 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -18,7 +18,7 @@ loader registers: ```text HotCRM v2.2.2 -Data: 18 Objects 345 Fields +Data: 18 Objects 334 Fields UI: 1 Apps 14 Views 8 Pages 5 Dashboards 10 Reports 30 Actions Logic: 26 Flows Security: 12 Positions 6 Permissions diff --git a/docs/developers/api_reference.md b/docs/developers/api_reference.md index eb8988423..595181e7c 100644 --- a/docs/developers/api_reference.md +++ b/docs/developers/api_reference.md @@ -12,7 +12,7 @@ Source: `src/objects/account.object.ts` Key fields: -`account_number`, `name`, `type`, `industry`, `annual_revenue`, `number_of_employees`, `phone`, `website`, `billing_address`, `office_location`, `owner`, `parent_account`, `is_active`, `tier`, `segment`, `health_score` +`account_number`, `name`, `type`, `industry`, `annual_revenue`, `child_account_revenue`, `number_of_employees`, `phone`, `website`, `billing_address`, `office_location`, `owner`, `parent_account`, `is_active`, `tier`, `segment`, `health_score` ### `crm_contact` - Contact @@ -20,7 +20,7 @@ Source: `src/objects/contact.object.ts` Key fields: -`salutation`, `first_name`, `last_name`, `full_name`, `avatar`, `crm_account`, `title`, `department`, `reports_to`, `owner`, `email`, `phone`, `mobile`, `mailing_street`, `mailing_city`, `mailing_state`, `mailing_postal_code`, `mailing_country`, `birthdate`, `lead_source`, `is_primary`, `do_not_call`, `email_opt_out` +`salutation`, `first_name`, `last_name`, `full_name`, `avatar`, `crm_account`, `title`, `department`, `owner`, `email`, `phone`, `mobile`, `mailing_street`, `mailing_city`, `mailing_state`, `mailing_postal_code`, `mailing_country`, `lead_source`, `is_primary`, `do_not_call`, `email_opt_out` ### `crm_lead` - Lead @@ -62,7 +62,7 @@ Source: `src/objects/case.object.ts` Key fields: -`case_number`, `subject`, `description`, `crm_account`, `crm_contact`, `status`, `priority`, `type`, `origin`, `owner`, `created_date`, `closed_date`, `first_response_date`, `resolution_time_hours`, `sla_due_date`, `is_sla_violated`, `is_escalated`, `escalated_date`, `escalation_reason`, `parent_case`, `resolution`, `customer_rating`, `customer_feedback`, `customer_signature`, `internal_notes`, `is_closed` +`case_number`, `subject`, `description`, `crm_account`, `crm_contact`, `status`, `priority`, `type`, `origin`, `owner`, `created_date`, `closed_date`, `first_response_date`, `resolution_time_hours`, `sla_due_date`, `is_sla_violated`, `is_escalated`, `escalated_date`, `escalation_reason`, `resolution`, `customer_rating`, `customer_feedback`, `internal_notes`, `is_closed` ### `crm_knowledge_article` - Knowledge Article @@ -78,7 +78,7 @@ Source: `src/objects/task.object.ts` Key fields: -`subject`, `description`, `status`, `priority`, `type`, `due_date`, `reminder_date`, `completed_date`, `owner`, `related_to_type`, `related_to_account`, `related_to_contact`, `related_to_opportunity`, `related_to_lead`, `related_to_case`, `is_recurring`, `recurrence_type`, `recurrence_interval`, `recurrence_end_date`, `is_completed`, `is_overdue`, `progress_percent`, `estimated_hours`, `actual_hours` +`subject`, `description`, `status`, `priority`, `type`, `due_date`, `reminder_date`, `completed_date`, `owner`, `related_to_type`, `related_to_account`, `related_to_contact`, `related_to_opportunity`, `related_to_lead`, `related_to_case`, `is_recurring`, `recurrence_type`, `recurrence_interval`, `recurrence_end_date`, `is_completed`, `is_overdue`, `progress_percent` ## Marketing @@ -88,7 +88,7 @@ Source: `src/objects/campaign.object.ts` Key fields: -`campaign_code`, `name`, `description`, `type`, `channel`, `status`, `start_date`, `end_date`, `budgeted_cost`, `actual_cost`, `expected_revenue`, `actual_revenue`, `target_size`, `num_sent`, `num_responses`, `num_leads`, `num_converted_leads`, `num_opportunities`, `num_won_opportunities`, `response_rate`, `roi`, `parent_campaign`, `owner`, `landing_page_url`, `is_active` +`campaign_code`, `name`, `description`, `type`, `channel`, `status`, `start_date`, `end_date`, `budgeted_cost`, `actual_cost`, `expected_revenue`, `actual_revenue`, `target_size`, `num_sent`, `num_responses`, `num_leads`, `num_converted_leads`, `num_opportunities`, `num_won_opportunities`, `response_rate`, `roi`, `owner`, `landing_page_url`, `is_active` ### `crm_campaign_member` - Campaign Member @@ -106,7 +106,7 @@ Source: `src/objects/product.object.ts` Key fields: -`product_code`, `name`, `description`, `category`, `family`, `list_price`, `cost`, `sku`, `quantity_on_hand`, `reorder_point`, `is_active`, `is_taxable`, `product_manager`, `image`, `datasheet`, `tax_rate`, `billing_type`, `unit_of_measure` +`product_code`, `name`, `description`, `category`, `family`, `list_price`, `cost`, `sku`, `is_active`, `product_manager`, `image`, `datasheet`, `tax_rate` ### `crm_quote` - Quote diff --git a/src/data/catalog.seed.ts b/src/data/catalog.seed.ts index 973256aa5..f8d36af4e 100644 --- a/src/data/catalog.seed.ts +++ b/src/data/catalog.seed.ts @@ -33,8 +33,6 @@ export const products = defineSeed(Product, { sku: 'OS-PLAT-ENT', list_price: 50000, cost: 12000, - billing_type: 'annual', - unit_of_measure: 'license', is_active: true, }, { @@ -45,8 +43,6 @@ export const products = defineSeed(Product, { sku: 'OS-PLAT-SMB', list_price: 18000, cost: 4500, - billing_type: 'annual', - unit_of_measure: 'license', is_active: true, }, { @@ -57,8 +53,6 @@ export const products = defineSeed(Product, { sku: 'OS-CLOUD-HOST', list_price: 12000, cost: 4200, - billing_type: 'annual', - unit_of_measure: 'each', is_active: true, }, { @@ -69,8 +63,6 @@ export const products = defineSeed(Product, { sku: 'OS-CLOUD-SBX', list_price: 7500, cost: 2600, - billing_type: 'annual', - unit_of_measure: 'each', is_active: true, }, { @@ -81,8 +73,6 @@ export const products = defineSeed(Product, { sku: 'OS-AI-SEAT', list_price: 1000, cost: 260, - billing_type: 'annual', - unit_of_measure: 'seat', is_active: true, }, { @@ -93,8 +83,6 @@ export const products = defineSeed(Product, { sku: 'OS-ADDON-ANL', list_price: 22000, cost: 6500, - billing_type: 'annual', - unit_of_measure: 'license', is_active: true, }, { @@ -105,8 +93,6 @@ export const products = defineSeed(Product, { sku: 'OS-ADDON-INT', list_price: 16000, cost: 5200, - billing_type: 'annual', - unit_of_measure: 'license', is_active: true, }, { @@ -117,8 +103,6 @@ export const products = defineSeed(Product, { sku: 'OS-ADDON-FSM', list_price: 14000, cost: 4200, - billing_type: 'annual', - unit_of_measure: 'license', is_active: true, }, { @@ -129,8 +113,6 @@ export const products = defineSeed(Product, { sku: 'OS-SUP-PREM', list_price: 25000, cost: 9000, - billing_type: 'annual', - unit_of_measure: 'each', is_active: true, }, { @@ -141,8 +123,6 @@ export const products = defineSeed(Product, { sku: 'OS-SUP-STD', list_price: 9000, cost: 3600, - billing_type: 'annual', - unit_of_measure: 'each', is_active: true, }, { @@ -153,8 +133,6 @@ export const products = defineSeed(Product, { sku: 'OS-SVC-IMPL', list_price: 75000, cost: 41000, - billing_type: 'one_time', - unit_of_measure: 'each', is_active: true, }, { @@ -165,8 +143,6 @@ export const products = defineSeed(Product, { sku: 'OS-SVC-MIGR', list_price: 35000, cost: 19000, - billing_type: 'one_time', - unit_of_measure: 'each', is_active: true, }, { @@ -177,8 +153,6 @@ export const products = defineSeed(Product, { sku: 'OS-SVC-TRN', list_price: 6000, cost: 2400, - billing_type: 'one_time', - unit_of_measure: 'day', is_active: true, }, ] diff --git a/src/mappings/contact_import.mapping.ts b/src/mappings/contact_import.mapping.ts index 5938cc0aa..b9447b234 100644 --- a/src/mappings/contact_import.mapping.ts +++ b/src/mappings/contact_import.mapping.ts @@ -48,8 +48,6 @@ export const ContactImportMapping = defineMapping({ { source: 'Mailing Postal Code', target: 'mailing_postal_code' }, { source: 'Mailing Country', target: 'mailing_country' }, - { source: 'Birthdate', target: 'birthdate' }, - { source: 'Lead Source', target: 'lead_source', diff --git a/src/objects/account.object.ts b/src/objects/account.object.ts index 32c21880f..d8af2419d 100644 --- a/src/objects/account.object.ts +++ b/src/objects/account.object.ts @@ -206,6 +206,42 @@ export const Account = ObjectSchema.create({ trackHistory: true, }), + /** + * Rolled-up annual revenue of this account's direct children. + * + * MEASURED against the installed engine (17.0.0) before being declared, on + * all four legs of the lifecycle — a roll-up that is only right at insert + * is a stale number, which is the same defect one layer over: + * + * insert two children (1,000 + 2,500) -> 3,500 + * raise one child to 4,000 -> 6,500 + * re-parent the other child away -> 4,000 + * delete the remaining child -> 0 + * + * Self-referencing roll-ups are the case worth measuring rather than + * assuming, because parent and child are the same object and the engine's + * summary index is keyed by child object. `test/decorative-field-sweep.test.ts` + * re-runs that measurement so a platform regression cannot quietly turn + * this back into the decoration it replaced. + * + * DIRECT children only, one level — the engine aggregates over the rows + * whose `parent_account` is this account, not over a transitive closure. A + * grandparent therefore shows the sum of its own children, and the docs say + * so; anything else would need a different (and much more expensive) + * primitive than the one the platform ships. + */ + child_account_revenue: Field.summary({ + label: 'Child Account Revenue', + group: 'financials', + scale: 2, + summaryOperations: { + object: 'crm_account', + field: 'annual_revenue', + function: 'sum', + relationshipField: 'parent_account', + }, + }), + number_of_employees: Field.number({ label: 'Employees', min: 0, @@ -329,6 +365,17 @@ export const Account = ObjectSchema.create({ // Relationship fields + /** + * The account hierarchy — kept because it now has a reader. + * + * This lookup was declared and traversed by nothing, while + * `content/docs/sales/accounts` sold the hierarchy with a diagram and + * promised "roll-up reports — annual revenue of the global parent sums all + * children". `child_account_revenue` below is that promise, and it is the + * whole of the enforcement: the platform computes roll-up summaries + * itself, so the honest consumer here is one declaration rather than the + * hand-written recompute hooks the line-item totals need. + */ parent_account: Field.lookup('crm_account', { label: 'Parent Account', description: 'Parent company in hierarchy', diff --git a/src/objects/campaign.object.ts b/src/objects/campaign.object.ts index 72c7d7a73..518dfab6a 100644 --- a/src/objects/campaign.object.ts +++ b/src/objects/campaign.object.ts @@ -235,13 +235,12 @@ export const Campaign = ObjectSchema.create({ scale: 2, }), - // Relationships - parent_campaign: Field.lookup('crm_campaign', { - group: 'basic', - label: 'Parent Campaign', - description: 'Parent campaign in hierarchy', - }), - + // No `parent_campaign`. Campaign hierarchy was declared and never used: no + // page, doc or report mentioned it, and the honest consumer for it — a + // rolled-up ROI — cannot be one declaration. `roi` here is a formula over + // this campaign's OWN `actual_cost` / `actual_revenue`, so a hierarchy ROI + // would put a second, differently-scoped ROI on the same record and leave + // every reader to guess which one they were looking at. // Campaign Assets landing_page_url: Field.url({ diff --git a/src/objects/case.object.ts b/src/objects/case.object.ts index 19d4eb7bc..335511be3 100644 --- a/src/objects/case.object.ts +++ b/src/objects/case.object.ts @@ -263,12 +263,11 @@ export const Case = ObjectSchema.create({ group: 'escalation', }), - // Related case - parent_case: Field.lookup('crm_case', { - label: 'Parent Case', - group: 'basic', - description: 'Related parent case', - }), + // No `parent_case`. A case hierarchy was declared here and never + // traversed: nothing rolled child cases up, nothing closed or notified them + // with the parent, and no service flow read the link. Related cases that + // matter in this app are linked through the knowledge article that resolved + // them (`resolved_by_article`), which the deflection measures do read. // Resolution resolution: Field.markdown({ @@ -313,13 +312,6 @@ export const Case = ObjectSchema.create({ group: 'resolution', }), - // Customer signature (for case resolution acknowledgment) - customer_signature: Field.signature({ - label: 'Customer Signature', - group: 'resolution', - description: 'Digital signature acknowledging case resolution', - }), - // Internal notes internal_notes: Field.markdown({ label: 'Internal Notes', diff --git a/src/objects/contact.object.ts b/src/objects/contact.object.ts index e675a25d5..c11bc3386 100644 --- a/src/objects/contact.object.ts +++ b/src/objects/contact.object.ts @@ -106,13 +106,10 @@ export const Contact = ObjectSchema.create({ ] }), - // Relationship fields - reports_to: Field.lookup('crm_contact', { - label: 'Reports To', - description: 'Direct manager/supervisor', - group: 'account_info', - }), - + // No `reports_to`. The org chart was declared and never built: no page + // rendered the tree the contact docs promised, no skill read the chain when + // summarising an account, and no flow escalated along it. Sharing derives + // from the master `crm_account`, never from this lookup. // Contact Information email: Field.email({ @@ -143,10 +140,11 @@ export const Contact = ObjectSchema.create({ mailing_country: Field.text({ label: 'Mailing Country', group: 'mailing_address' }), // Additional Information - birthdate: Field.date({ - label: 'Birthdate', - group: 'additional', - }), + // + // No `birthdate`. Nothing greeted, segmented or reported on it, and an + // importable date of birth with no consumer is personal data held for no + // stated purpose — the one field on this object where "declared but inert" + // also carries a data-protection cost. lead_source: Field.select({ label: 'Lead Source', diff --git a/src/objects/product.object.ts b/src/objects/product.object.ts index 9afe7959e..d5ea1b5c6 100644 --- a/src/objects/product.object.ts +++ b/src/objects/product.object.ts @@ -29,7 +29,7 @@ export const Product = ObjectSchema.create({ fieldGroups: [ { key: 'basic', label: 'Product Information', icon: 'info' }, - { key: 'pricing', label: 'Pricing & Billing', icon: 'dollar-sign' }, + { key: 'pricing', label: 'Pricing', icon: 'dollar-sign' }, { key: 'metadata', label: 'Resources', icon: 'link', defaultExpanded: false }, ], @@ -104,7 +104,13 @@ export const Product = ObjectSchema.create({ min: 0, }), - // SKU and Inventory + // SKU + // + // `quantity_on_hand` / `reorder_point` were removed with the rest of the + // inventory surface: nothing in this app decremented stock, and the "Low + // Stock" view they fed compared against a hardcoded 10 rather than the + // reorder point beside it. HotCRM sells from a catalog; warehouse state + // belongs to the system of record that owns it. sku: Field.text({ label: 'SKU', group: 'basic', @@ -112,19 +118,6 @@ export const Product = ObjectSchema.create({ unique: true, }), - quantity_on_hand: Field.number({ - label: 'Quantity on Hand', - group: 'basic', - min: 0, - defaultValue: 0, - }), - - reorder_point: Field.number({ - label: 'Reorder Point', - group: 'basic', - min: 0, - }), - // Status is_active: Field.boolean({ label: 'Active', @@ -132,12 +125,6 @@ export const Product = ObjectSchema.create({ defaultValue: true, trackHistory: true, }), - - is_taxable: Field.boolean({ - label: 'Taxable', - group: 'pricing', - defaultValue: true, - }), // Relationships product_manager: Field.lookup('sys_user', { @@ -174,30 +161,12 @@ export const Product = ObjectSchema.create({ defaultValue: 0, }), - billing_type: Field.select({ - label: 'Billing Type', - group: 'pricing', - options: [ - { label: 'One-Time', value: 'one_time', default: true }, - { label: 'Monthly', value: 'monthly' }, - { label: 'Quarterly', value: 'quarterly' }, - { label: 'Annual', value: 'annual' }, - { label: 'Usage', value: 'usage' }, - ], - }), - - unit_of_measure: Field.select({ - label: 'Unit of Measure', - group: 'pricing', - options: [ - { label: 'Each', value: 'each', default: true }, - { label: 'License', value: 'license' }, - { label: 'Seat', value: 'seat' }, - { label: 'Hour', value: 'hour' }, - { label: 'Day', value: 'day' }, - { label: 'Month', value: 'month' }, - ], - }), + // `billing_type` and `unit_of_measure` were removed with the tax flag: both + // were seeded on all 13 catalog products and read by nothing — no quote + // total, no revenue-recognition report and no line-item behaviour ever + // consulted either. A picklist that only ever renders itself is a + // maintenance obligation (four locales, an import column, every future + // migration) charged against a capability that does not exist. }, // Database indexes diff --git a/src/objects/task.object.ts b/src/objects/task.object.ts index fc21280f9..31a6fff3e 100644 --- a/src/objects/task.object.ts +++ b/src/objects/task.object.ts @@ -239,20 +239,10 @@ export const Task = ObjectSchema.create({ defaultValue: 0, }), - // Time tracking - estimated_hours: Field.number({ - group: 'effort', - label: 'Estimated Hours', - scale: 2, - min: 0, - }), - - actual_hours: Field.number({ - group: 'effort', - label: 'Actual Hours', - scale: 2, - min: 0, - }), + // No time tracking. `estimated_hours` / `actual_hours` were declared and + // inert: no rollup summed them onto a case or an opportunity, no variance + // report compared them, and nothing warned when actual overran estimate. + // Effort on a task is `progress_percent`, which the task views do read. }, // Dead object-level enable.* flags removed in @objectstack 12 (ADR-0049); diff --git a/src/translations/en.ts b/src/translations/en.ts index 666bdfaf5..eab6765a1 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -79,6 +79,7 @@ export const en: TranslationData = { }, }, annual_revenue: { label: 'Annual Revenue' }, + child_account_revenue: { label: 'Child Account Revenue', help: 'Sum of the annual revenue of the direct children of this account.' }, number_of_employees: { label: 'Number of Employees' }, phone: { label: 'Phone' }, website: { label: 'Website' }, @@ -171,13 +172,11 @@ export const en: TranslationData = { description: { label: 'Description' }, is_primary: { label: 'Primary Contact', help: 'Is this the main contact for the account?' }, avatar: { label: 'Profile Picture' }, - reports_to: { label: 'Reports To', help: 'Direct manager/supervisor' }, mailing_street: { label: 'Mailing Street' }, mailing_city: { label: 'Mailing City' }, mailing_state: { label: 'Mailing State/Province' }, mailing_postal_code: { label: 'Mailing Postal Code' }, mailing_country: { label: 'Mailing Country' }, - birthdate: { label: 'Birthdate' }, lead_source: { label: 'Lead Source', options: { @@ -729,7 +728,6 @@ export const en: TranslationData = { is_sla_violated: { label: 'SLA Violated' }, is_escalated: { label: 'Escalated' }, escalation_reason: { label: 'Escalation Reason' }, - parent_case: { label: 'Parent Case', help: 'Related parent case' }, resolution: { label: 'Resolution' }, resolved_by_article: { label: 'Resolved by Article', help: 'Knowledge article that resolved this case — the deflection signal.' }, customer_rating: { @@ -737,10 +735,6 @@ export const en: TranslationData = { help: 'Customer satisfaction rating (1-5 stars)', }, customer_feedback: { label: 'Customer Feedback' }, - customer_signature: { - label: 'Customer Signature', - help: 'Digital signature acknowledging case resolution', - }, internal_notes: { label: 'Internal Notes', help: 'Internal notes; not visible to the customer.' }, is_closed: { label: 'Is Closed' }, display_title: { label: 'Display Title' }, @@ -889,25 +883,8 @@ export const en: TranslationData = { }, list_price: { label: 'List Price' }, cost: { label: 'Cost' }, - billing_type: { - label: 'Billing Type', - options: { - one_time: 'One-Time', monthly: 'Monthly', quarterly: 'Quarterly', - annual: 'Annual', usage: 'Usage', - }, - }, - unit_of_measure: { - label: 'Unit of Measure', - options: { - each: 'Each', license: 'License', seat: 'Seat', - hour: 'Hour', day: 'Day', month: 'Month', - }, - }, sku: { label: 'SKU' }, - quantity_on_hand: { label: 'Quantity on Hand' }, - reorder_point: { label: 'Reorder Point' }, is_active: { label: 'Active' }, - is_taxable: { label: 'Taxable' }, product_manager: { label: 'Product Manager' }, image: { label: 'Product Image' }, datasheet: { label: 'Datasheet' }, @@ -917,15 +894,14 @@ export const en: TranslationData = { _views: { all_products: { label: 'All Products' }, product_catalog: { label: 'Product Catalog' }, - low_stock: { label: 'Low Stock' }, }, _sections: { basic: { label: 'Product Information' }, - pricing: { label: 'Pricing & Billing' }, + pricing: { label: 'Pricing' }, metadata: { label: 'Resources' }, // Form section names on product.view.ts (#1100) product_info: { label: 'Product Info' }, - pricing_and_inventory: { label: 'Pricing & Inventory' }, + pricing_info: { label: 'Pricing' }, media: { label: 'Media' }, }, }, @@ -1042,8 +1018,6 @@ export const en: TranslationData = { is_completed: { label: 'Is Completed' }, is_overdue: { label: 'Is Overdue' }, progress_percent: { label: 'Progress (%)' }, - estimated_hours: { label: 'Estimated Hours' }, - actual_hours: { label: 'Actual Hours' }, priority_rank: { label: 'Priority Rank' }, reminder_sent: { label: 'Reminder Sent' }, }, @@ -1115,7 +1089,6 @@ export const en: TranslationData = { num_won_opportunities: { label: 'Won Opportunities' }, response_rate: { label: 'Response Rate %' }, roi: { label: 'ROI %' }, - parent_campaign: { label: 'Parent Campaign', help: 'Parent campaign in hierarchy' }, owner_id: { label: 'Campaign Owner' }, landing_page_url: { label: 'Landing Page' }, is_active: { label: 'Active' }, diff --git a/src/translations/es-ES.ts b/src/translations/es-ES.ts index 997fcc42e..93f785105 100644 --- a/src/translations/es-ES.ts +++ b/src/translations/es-ES.ts @@ -86,6 +86,7 @@ export const esES: TranslationData = { }, }, annual_revenue: { label: 'Ingresos Anuales' }, + child_account_revenue: { label: 'Ingresos de Cuentas Hijas', help: 'Suma de los ingresos anuales de las cuentas hijas directas.' }, number_of_employees: { label: 'Número de Empleados' }, phone: { label: 'Teléfono' }, website: { label: 'Sitio Web' }, @@ -184,13 +185,11 @@ export const esES: TranslationData = { description: { label: 'Descripción' }, is_primary: { label: 'Contacto Principal', help: '¿Es este el contacto principal de la cuenta?' }, avatar: { label: 'Foto de Perfil' }, - reports_to: { label: 'Reporta A', help: 'Jefe o supervisor directo' }, mailing_street: { label: 'Calle de Correo' }, mailing_city: { label: 'Ciudad de Correo' }, mailing_state: { label: 'Estado/Provincia de Correo' }, mailing_postal_code: { label: 'Código Postal' }, mailing_country: { label: 'País de Correo' }, - birthdate: { label: 'Fecha de Nacimiento' }, // Juego compartido con `crm_lead.lead_source` y // `crm_opportunity.lead_source` — los tres deben coincidir literalmente. lead_source: { @@ -759,12 +758,10 @@ export const esES: TranslationData = { is_escalated: { label: 'Escalado' }, escalated_date: { label: 'Fecha de Escalación' }, escalation_reason: { label: 'Motivo de Escalación' }, - parent_case: { label: 'Caso Principal', help: 'Caso principal relacionado' }, resolution: { label: 'Resolución' }, resolved_by_article: { label: 'Resuelto por Artículo', help: 'Artículo de la base de conocimiento que resolvió este caso — la señal de desvío.' }, customer_rating: { label: 'Satisfacción del Cliente', help: 'Valoración de satisfacción del cliente (1-5 estrellas)' }, customer_feedback: { label: 'Comentarios del Cliente' }, - customer_signature: { label: 'Firma del Cliente', help: 'Firma digital que confirma la resolución del caso' }, internal_notes: { label: 'Notas Internas', help: 'Notas internas no visibles para el cliente' }, is_closed: { label: 'Está Cerrado' }, }, @@ -917,25 +914,8 @@ export const esES: TranslationData = { list_price: { label: 'Precio de Lista' }, cost: { label: 'Costo' }, sku: { label: 'SKU' }, - quantity_on_hand: { label: 'Cantidad Disponible' }, - reorder_point: { label: 'Punto de Reorden' }, is_active: { label: 'Activo' }, - is_taxable: { label: 'Imponible' }, tax_rate: { label: 'Tasa de Impuesto por Defecto %' }, - billing_type: { - label: 'Tipo de Facturación', - options: { - one_time: 'Pago Único', monthly: 'Mensual', quarterly: 'Trimestral', - annual: 'Anual', usage: 'Por Uso', - }, - }, - unit_of_measure: { - label: 'Unidad de Medida', - options: { - each: 'Unidad', license: 'Licencia', seat: 'Puesto', - hour: 'Hora', day: 'Día', month: 'Mes', - }, - }, product_manager: { label: 'Gerente de Producto' }, image: { label: 'Imagen de Producto' }, datasheet: { label: 'Ficha Técnica' }, @@ -943,15 +923,14 @@ export const esES: TranslationData = { _views: { all_products: { label: 'Todos los Productos' }, product_catalog: { label: 'Catálogo de Productos' }, - low_stock: { label: 'Stock Bajo' }, }, _sections: { basic: { label: 'Información del Producto' }, - pricing: { label: 'Precios y Facturación' }, + pricing: { label: 'Precios' }, metadata: { label: 'Recursos' }, // Nombres de sección del formulario en product.view.ts (#1100) product_info: { label: 'Información del Producto' }, - pricing_and_inventory: { label: 'Precios e Inventario' }, + pricing_info: { label: 'Precios' }, media: { label: 'Multimedia' }, }, }, @@ -1075,8 +1054,6 @@ export const esES: TranslationData = { is_completed: { label: 'Está Completado' }, is_overdue: { label: 'Está Vencido' }, progress_percent: { label: 'Progreso (%)' }, - estimated_hours: { label: 'Horas Estimadas' }, - actual_hours: { label: 'Horas Reales' }, }, _views: { all_tasks: { label: 'Todas las Tareas' }, @@ -1148,7 +1125,6 @@ export const esES: TranslationData = { num_won_opportunities: { label: 'Oportunidades Ganadas' }, response_rate: { label: 'Tasa de Respuesta %' }, roi: { label: 'ROI %' }, - parent_campaign: { label: 'Campaña Principal', help: 'Campaña principal en la jerarquía' }, owner_id: { label: 'Propietario de Campaña' }, landing_page_url: { label: 'Página de Aterrizaje' }, is_active: { label: 'Activo' }, diff --git a/src/translations/ja-JP.ts b/src/translations/ja-JP.ts index 8f35fb427..caee847ad 100644 --- a/src/translations/ja-JP.ts +++ b/src/translations/ja-JP.ts @@ -116,6 +116,7 @@ export const jaJP: TranslationData = { options: { ...industryOptions }, }, annual_revenue: { label: '年間売上' }, + child_account_revenue: { label: '子会社年間売上合計', help: '直属の子取引先の年間売上の合計。' }, number_of_employees: { label: '従業員数' }, phone: { label: '電話番号' }, website: { label: 'Webサイト' }, @@ -204,13 +205,11 @@ export const jaJP: TranslationData = { description: { label: '説明' }, is_primary: { label: '主担当者', help: 'この取引先の主担当者かどうか' }, avatar: { label: 'プロフィール画像' }, - reports_to: { label: '上司', help: '直属の上司・管理者' }, mailing_street: { label: '郵送先 番地' }, mailing_city: { label: '郵送先 市区町村' }, mailing_state: { label: '郵送先 都道府県' }, mailing_postal_code: { label: '郵便番号' }, mailing_country: { label: '郵送先 国' }, - birthdate: { label: '生年月日' }, lead_source: { label: 'リードソース', options: { ...leadSourceOptions } }, do_not_call: { label: '電話拒否' }, email_opt_out: { label: 'メール配信停止' }, @@ -733,12 +732,10 @@ export const jaJP: TranslationData = { is_escalated: { label: 'エスカレーション済' }, escalated_date: { label: 'エスカレーション日' }, escalation_reason: { label: 'エスカレーション理由' }, - parent_case: { label: '親ケース', help: '関連する親ケース' }, resolution: { label: '解決内容' }, resolved_by_article: { label: '解決したナレッジ記事', help: 'このケースを解決したナレッジ記事 — 逸らし率の指標。' }, customer_rating: { label: '顧客満足度', help: '顧客満足度の評価(1〜5 段階)' }, customer_feedback: { label: '顧客フィードバック' }, - customer_signature: { label: '顧客署名', help: 'ケースの解決を確認する電子署名' }, internal_notes: { label: '内部メモ', help: '顧客には表示されない社内向けメモ' }, is_closed: { label: 'クローズ済' }, }, @@ -881,25 +878,8 @@ export const jaJP: TranslationData = { list_price: { label: '定価' }, cost: { label: 'コスト' }, sku: { label: 'SKU' }, - quantity_on_hand: { label: '在庫数' }, - reorder_point: { label: '発注点' }, is_active: { label: '有効' }, - is_taxable: { label: '課税対象' }, tax_rate: { label: '既定の税率 %' }, - billing_type: { - label: '課金タイプ', - options: { - one_time: '一括', monthly: '月次', quarterly: '四半期', - annual: '年次', usage: '従量課金', - }, - }, - unit_of_measure: { - label: '単位', - options: { - each: '個', license: 'ライセンス', seat: 'シート', - hour: '時間', day: '日', month: '月', - }, - }, product_manager: { label: 'プロダクトマネージャー' }, image: { label: '製品画像' }, datasheet: { label: 'データシート' }, @@ -907,15 +887,14 @@ export const jaJP: TranslationData = { _views: { all_products: { label: '全製品' }, product_catalog: { label: '製品カタログ' }, - low_stock: { label: '低在庫' }, }, _sections: { basic: { label: '製品情報' }, - pricing: { label: '価格・請求' }, + pricing: { label: '価格' }, metadata: { label: '関連資料' }, // product.view.ts のフォームセクション名 (#1100) product_info: { label: '製品情報' }, - pricing_and_inventory: { label: '価格と在庫' }, + pricing_info: { label: '価格' }, media: { label: 'メディア' }, }, }, @@ -1026,8 +1005,6 @@ export const jaJP: TranslationData = { is_completed: { label: '完了済' }, is_overdue: { label: '期限超過' }, progress_percent: { label: '進捗(%)' }, - estimated_hours: { label: '見積工数' }, - actual_hours: { label: '実績工数' }, }, _views: { all_tasks: { label: '全タスク' }, @@ -1100,7 +1077,6 @@ export const jaJP: TranslationData = { num_won_opportunities: { label: '受注商談' }, response_rate: { label: '応答率(%)' }, roi: { label: 'ROI(%)' }, - parent_campaign: { label: '親キャンペーン', help: '階層構造における上位のキャンペーン' }, owner_id: { label: 'キャンペーン担当者' }, landing_page_url: { label: 'ランディングページ' }, is_active: { label: '有効' }, diff --git a/src/translations/zh-CN.ts b/src/translations/zh-CN.ts index 6aab75c01..f1028e81f 100644 --- a/src/translations/zh-CN.ts +++ b/src/translations/zh-CN.ts @@ -76,6 +76,7 @@ export const zhCN: TranslationData = { }, }, annual_revenue: { label: '年营收' }, + child_account_revenue: { label: '子公司年营收合计', help: '本客户直属子公司的年营收合计。' }, number_of_employees: { label: '员工人数' }, phone: { label: '电话' }, website: { label: '网站' }, @@ -165,13 +166,11 @@ export const zhCN: TranslationData = { owner_id: { label: '联系人负责人' }, description: { label: '描述' }, is_primary: { label: '主要联系人', help: '是否为该客户的主要联系人?' }, - reports_to: { label: '直属上级', help: '直属上级/主管' }, mailing_street: { label: '邮寄地址' }, mailing_city: { label: '邮寄城市' }, mailing_state: { label: '邮寄省份' }, mailing_postal_code: { label: '邮政编码' }, mailing_country: { label: '邮寄国家' }, - birthdate: { label: '生日' }, lead_source: { label: '线索来源', options: { @@ -691,12 +690,10 @@ export const zhCN: TranslationData = { is_sla_violated: { label: 'SLA 违反' }, is_escalated: { label: '已升级' }, escalation_reason: { label: '升级原因' }, - parent_case: { label: '父级工单', help: '关联的父工单' }, resolution: { label: '解决方案' }, resolved_by_article: { label: '解决该工单的知识文章', help: '解决此工单的知识库文章 —— 转移率的统计口径。' }, customer_rating: { label: '客户满意度', help: '客户满意度评分(1-5 星)' }, customer_feedback: { label: '客户反馈' }, - customer_signature: { label: '客户签字', help: '确认工单已解决的电子签名' }, internal_notes: { label: '内部备注', help: '内部备注,客户不可见' }, is_closed: { label: '是否关闭' }, display_title: { label: '显示名称' }, @@ -803,8 +800,6 @@ export const zhCN: TranslationData = { is_completed: { label: '是否完成' }, is_overdue: { label: '是否逾期' }, progress_percent: { label: '进度(%)' }, - estimated_hours: { label: '预估工时' }, - actual_hours: { label: '实际工时' }, priority_rank: { label: '优先级排序' }, reminder_sent: { label: '提醒已发送' }, }, @@ -874,7 +869,6 @@ export const zhCN: TranslationData = { num_won_opportunities: { label: '已赢得商机' }, response_rate: { label: '响应率(%)' }, roi: { label: '投资回报率(%)' }, - parent_campaign: { label: '父级活动', help: '层级结构中的上级活动' }, owner_id: { label: '活动负责人' }, landing_page_url: { label: '着陆页' }, is_active: { label: '是否启用' }, @@ -917,20 +911,6 @@ export const zhCN: TranslationData = { }, }, cost: { label: '成本' }, - billing_type: { - label: '计费类型', - options: { - one_time: '一次性', monthly: '按月', quarterly: '按季度', - annual: '按年', usage: '按用量', - }, - }, - unit_of_measure: { - label: '计量单位', - options: { - each: '个', license: '许可证', seat: '席位', - hour: '小时', day: '天', month: '月', - }, - }, is_active: { label: '是否启用' }, description: { label: '描述' }, family: { @@ -942,9 +922,6 @@ export const zhCN: TranslationData = { }, list_price: { label: '标价' }, sku: { label: 'SKU' }, - quantity_on_hand: { label: '库存数量' }, - reorder_point: { label: '补货阈值' }, - is_taxable: { label: '应税' }, product_manager: { label: '产品经理' }, image: { label: '产品图片' }, datasheet: { label: '规格书' }, @@ -954,15 +931,14 @@ export const zhCN: TranslationData = { _views: { all_products: { label: '全部产品' }, product_catalog: { label: '产品目录' }, - low_stock: { label: '低库存' }, }, _sections: { basic: { label: '产品信息' }, - pricing: { label: '价格与计费' }, + pricing: { label: '价格' }, metadata: { label: '资源' }, // product.view.ts 表单区块名称 (#1100) product_info: { label: '产品信息' }, - pricing_and_inventory: { label: '定价与库存' }, + pricing_info: { label: '定价' }, media: { label: '媒体' }, }, }, diff --git a/src/views/account.view.ts b/src/views/account.view.ts index 04ffacb1f..c1247ce41 100644 --- a/src/views/account.view.ts +++ b/src/views/account.view.ts @@ -208,10 +208,13 @@ export const AccountViews = defineView({ ], }, { + // `child_account_revenue` is a roll-up the engine maintains, so it is + // read-only on the form by construction — it is here because a hierarchy + // nobody can see the effect of is the decoration this field replaced. name: 'financials', label: 'Financials', columns: 2, - fields: ['annual_revenue', 'number_of_employees'], + fields: ['annual_revenue', 'number_of_employees', 'child_account_revenue'], }, { // The customer-success fields the at_risk_accounts view lists and diff --git a/src/views/campaign.view.ts b/src/views/campaign.view.ts index 0b679bba2..313f9440d 100644 --- a/src/views/campaign.view.ts +++ b/src/views/campaign.view.ts @@ -111,7 +111,6 @@ export const CampaignViews = defineView({ 'type', 'channel', { field: 'status', required: true }, - 'parent_campaign', 'owner_id', 'is_active', 'landing_page_url', diff --git a/src/views/case.view.ts b/src/views/case.view.ts index 3bb9b9827..59a430891 100644 --- a/src/views/case.view.ts +++ b/src/views/case.view.ts @@ -289,14 +289,13 @@ export const CaseViews = defineView({ 'is_sla_violated', 'is_escalated', 'escalation_reason', - 'parent_case', ], }, { name: 'resolution', label: 'Resolution', columns: 1, - fields: ['resolution', 'internal_notes', 'customer_rating', 'customer_feedback', 'customer_signature', 'closed_date', 'is_closed'], + fields: ['resolution', 'internal_notes', 'customer_rating', 'customer_feedback', 'closed_date', 'is_closed'], }, ], }, diff --git a/src/views/contact.view.ts b/src/views/contact.view.ts index 06c1112db..77eec503c 100644 --- a/src/views/contact.view.ts +++ b/src/views/contact.view.ts @@ -89,7 +89,6 @@ export const ContactViews = defineView({ { field: 'crm_account', required: true }, 'title', 'department', - 'reports_to', 'owner_id', ], }, @@ -102,7 +101,7 @@ export const ContactViews = defineView({ name: 'contact_details', label: 'Contact Info', columns: 2, - fields: ['email', 'phone', 'mobile', 'birthdate', 'avatar'], + fields: ['email', 'phone', 'mobile', 'avatar'], }, { // Named `comm_preferences`, not `preferences` — `preferences` is diff --git a/src/views/product.view.ts b/src/views/product.view.ts index 0a491660a..842e64dd5 100644 --- a/src/views/product.view.ts +++ b/src/views/product.view.ts @@ -5,8 +5,14 @@ import { defineView } from '@objectstack/spec/ui'; /** * Product Views * - * • grid — catalog listing with stock indicators + * • grid — catalog listing * • gallery — visual product catalog (`image` as cover) + * + * The `low_stock` view was removed with the inventory fields it was built on: + * its columns, its filter and its sort key were all `quantity_on_hand` / + * `reorder_point`, so it could not outlive them. It was never the stock report + * it looked like either — the filter compared against a hardcoded 10 rather + * than each product's own reorder point. */ export const ProductViews = defineView({ list: { @@ -22,8 +28,6 @@ export const ProductViews = defineView({ { field: 'sku', width: 140 }, { field: 'list_price', width: 130, align: 'right', summary: 'avg' }, { field: 'cost', width: 130, align: 'right' }, - { field: 'quantity_on_hand', width: 140, align: 'right', summary: 'sum' }, - { field: 'reorder_point', width: 130, align: 'right' }, { field: 'is_active', width: 100, align: 'center' }, ], sort: [{ field: 'name', order: 'asc' }], @@ -36,7 +40,6 @@ export const ProductViews = defineView({ tabs: [ { name: 'all', label: 'All', view: 'all_products', isDefault: true, pinned: true }, { name: 'catalog', label: 'Catalog', icon: 'gallery-thumbnails', view: 'product_catalog' }, - { name: 'low_stock', label: 'Low Stock', icon: 'triangle-alert', view: 'low_stock' }, ], }, @@ -53,21 +56,9 @@ export const ProductViews = defineView({ coverFit: 'cover', cardSize: 'medium', titleField: 'name', - visibleFields: ['product_code', 'category', 'family', 'list_price', 'quantity_on_hand'], + visibleFields: ['product_code', 'category', 'family', 'list_price', 'sku'], }, }, - - /** Low-stock review */ - low_stock: { - name: 'low_stock', - type: 'grid', - label: 'Low Stock', - data: { provider: 'object', object: 'crm_product' }, - columns: ['product_code', 'name', 'quantity_on_hand', 'reorder_point', 'product_manager'], - filter: [{ field: 'quantity_on_hand', operator: 'less_than_or_equal', value: 10 }], - sort: [{ field: 'quantity_on_hand', order: 'asc' }], - rowColor: { field: 'is_active', colors: { true: '#dc2626' } }, - }, }, form: { @@ -85,14 +76,13 @@ export const ProductViews = defineView({ 'sku', 'product_manager', 'is_active', - 'is_taxable', ], }, { - name: 'pricing_and_inventory', - label: 'Pricing & Inventory', + name: 'pricing_info', + label: 'Pricing', columns: 2, - fields: ['list_price', 'cost', 'quantity_on_hand', 'reorder_point'], + fields: ['list_price', 'cost'], }, { name: 'media', diff --git a/src/views/task.view.ts b/src/views/task.view.ts index 0b5d74713..8377ec67a 100644 --- a/src/views/task.view.ts +++ b/src/views/task.view.ts @@ -289,8 +289,6 @@ export const TaskViews = defineView({ 'recurrence_type', 'recurrence_interval', 'recurrence_end_date', - 'estimated_hours', - 'actual_hours', 'progress_percent', ], }, diff --git a/test/decorative-field-sweep.test.ts b/test/decorative-field-sweep.test.ts new file mode 100644 index 000000000..a8b17a293 --- /dev/null +++ b/test/decorative-field-sweep.test.ts @@ -0,0 +1,245 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { ObjectKernel } from '@objectstack/core'; +import { DefaultDatasourcePlugin, AppPlugin } from '@objectstack/runtime'; +import { ObjectQLPlugin } from '@objectstack/objectql'; +import { MetadataPlugin } from '@objectstack/metadata'; +import stack from '../objectstack.config'; +import { Product } from '../src/objects/product.object'; +import { Case } from '../src/objects/case.object'; +import { Task } from '../src/objects/task.object'; +import { Contact } from '../src/objects/contact.object'; +import { Campaign } from '../src/objects/campaign.object'; +import { Account } from '../src/objects/account.object'; +import { ProductViews } from '../src/views/product.view'; +import { CaseViews } from '../src/views/case.view'; +import { TaskViews } from '../src/views/task.view'; +import { ContactViews } from '../src/views/contact.view'; +import { CampaignViews } from '../src/views/campaign.view'; +import { AccountViews } from '../src/views/account.view'; +import { products } from '../src/data/catalog.seed'; +import { ContactImportMapping } from '../src/mappings/contact_import.mapping'; +import { AccountImportMapping } from '../src/mappings/account_import.mapping'; +import { localePacks, type AnyRec } from './helpers/metadata-fixtures'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { REPO_ROOT } from './helpers/repo-root'; + +/** + * Enforce-or-remove, decided per field (#1182). + * + * A consumer scan found ten rows of declared-but-inert fields, and the + * maintainer ruling of 2026-08-17 —「逐个 enforce-or-remove(推荐)」— asked for + * a verdict on each rather than one verdict for the card. Nine rows were + * removed. One was enforced, and the two halves of this file are the two + * things that can regress: + * + * 1. every removed field stays removed **everywhere it was read** — object, + * view, all four locale bundles, seed data and import mapping. A removal + * that leaves a reader behind does not fail the build: a stale column + * renders empty, a stale locale row reads as coverage for a field that is + * not there, and a stale import column fails a customer's file at run time. + * + * 2. `crm_account.parent_account` — the one row that was KEPT — still has the + * consumer it was kept for, and that consumer still WORKS. + * + * Half 2 is the half worth arguing for, and it is not a metadata assertion. + * The whole point of the enforce verdict is that the hierarchy stopped being + * decorative, and a test that only checked `child_account_revenue` is declared + * would pass just as happily on the day the platform stopped computing it — + * which would put the field back in exactly the state this card removed nine + * other fields for being in. So the roll-up is MEASURED against the real + * engine, on the whole lifecycle rather than at insert alone: a roll-up that is + * only right when the child is created is a stale number, which is the same + * defect one layer over. + */ + +// ───────────────────────────────────────── 1. the nine removals ── + +/** Every removed field, against the object it was declared on. */ +const REMOVED: Array<[string, AnyRec, readonly string[]]> = [ + ['crm_product', Product, ['quantity_on_hand', 'reorder_point', 'is_taxable', 'billing_type', 'unit_of_measure']], + ['crm_case', Case, ['customer_signature', 'parent_case']], + ['crm_task', Task, ['estimated_hours', 'actual_hours']], + ['crm_contact', Contact, ['reports_to', 'birthdate']], + ['crm_campaign', Campaign, ['parent_campaign']], +]; + +describe('the decorative fields are gone from their objects (#1182)', () => { + it.each(REMOVED.flatMap(([object, schema, fields]) => fields.map((f) => [object, f, schema] as const)))( + '%s.%s is no longer declared', + (_object, field, schema) => { + expect(Object.keys((schema as AnyRec).fields as AnyRec)).not.toContain(field); + }, + ); +}); + +describe('every reader of a removed field went with it (#1182)', () => { + /** + * The views are listed per object rather than as one blob so a failure names + * the surface that kept the reference. `localePacks` covers all four + * bundles at once — an English-only sweep is how three of these fields + * survived a previous cleanup with their zh-CN / es-ES / ja-JP rows intact. + */ + const SURFACES: Array<[string, unknown]> = [ + ['product object', Product], ['product views', ProductViews], + ['case object', Case], ['case views', CaseViews], + ['task object', Task], ['task views', TaskViews], + ['contact object', Contact], ['contact views', ContactViews], + ['campaign object', Campaign], ['campaign views', CampaignViews], + ['account object', Account], ['account views', AccountViews], + ['locale packs', localePacks], + ['catalog seed', products], + ['contact import mapping', ContactImportMapping], + ]; + + it.each(REMOVED.flatMap(([, , fields]) => fields))('%s is referenced by no shipped metadata', (field) => { + const offenders = SURFACES + .filter(([, value]) => JSON.stringify(value ?? null).includes(field)) + .map(([label]) => label); + expect(offenders, `${field} still read by: ${offenders.join(', ')}`).toEqual([]); + }); + + /** + * `low_stock` was a whole list view built on two of the removed fields — its + * columns, its `less_than_or_equal` filter and its sort key were all + * `quantity_on_hand` / `reorder_point` — so it could not outlive them. View + * and switcher tab are asserted together: a tab pointing at a deleted view + * and a view unreachable from any tab are two different broken states, and + * this removal must produce neither. + */ + it('drops the low_stock view and its switcher tab together', () => { + expect(Object.keys(ProductViews.listViews ?? {})).not.toContain('low_stock'); + const tabViews = ((ProductViews.list?.tabs ?? []) as AnyRec[]).map((t) => t.view); + expect(tabViews).not.toContain('low_stock'); + const declared = new Set(Object.keys(ProductViews.listViews ?? {})); + for (const view of tabViews) { + if (view === ProductViews.list?.name) continue; + expect(declared, `tab points at missing view "${view}"`).toContain(view); + } + }); + + /** + * The import template is the one surface where a leftover row fails a + * CUSTOMER rather than a developer: an unremoved `Birthdate` column would + * keep asking for data the object can no longer store. + * + * Both halves of the template are asserted, because they are two files. The + * mapping is metadata under `src/`; the sample CSV shipped to customers is + * under `assets/import-templates/`, outside every `src/`-scoped scan — and it + * is exactly where this removal was first left half-done. `import-mappings` + * caught it by comparing the two, which is why that comparison exists; this + * assertion names the field so a failure here says which removal regressed. + */ + it('the contact import template no longer asks for a birthdate', () => { + const targets = ((ContactImportMapping as AnyRec).fieldMapping ?? []).map((f: AnyRec) => f.target); + expect(targets).not.toContain('birthdate'); + expect(targets).toContain('lead_source'); // positive control: the mapping still has columns + + const csv = readFileSync(join(REPO_ROOT, 'assets/import-templates/contacts.csv'), 'utf8'); + const headers = csv.split('\n')[0].split(','); + expect(headers).not.toContain('Birthdate'); + expect(headers).toContain('Lead Source'); // positive control: the CSV still has headers + }); +}); + +// ──────────────────────────── 2. the one row that was ENFORCED ── + +describe('the account hierarchy was kept because it now has a consumer (#1182)', () => { + it('parent_account is still declared, and still importable', () => { + expect(Object.keys(Account.fields as AnyRec)).toContain('parent_account'); + const targets = ((AccountImportMapping as AnyRec).fieldMapping ?? []).map((f: AnyRec) => f.target); + expect(targets).toContain('parent_account'); + }); + + it('child_account_revenue rolls up annual_revenue over the parent link', () => { + const field = (Account.fields as AnyRec).child_account_revenue; + expect(field?.type).toBe('summary'); + expect(field?.summaryOperations).toMatchObject({ + object: 'crm_account', + field: 'annual_revenue', + function: 'sum', + relationshipField: 'parent_account', + }); + }); + + it('is on the account form, where a user can see the hierarchy do something', () => { + expect(JSON.stringify(AccountViews)).toContain('child_account_revenue'); + }); + + it('carries a label in all four locales', () => { + for (const [locale, pack] of localePacks) { + const label = (pack as AnyRec)?.objects?.crm_account?.fields?.child_account_revenue?.label; + expect(label, `${locale} has no label for child_account_revenue`).toBeTruthy(); + } + }); +}); + +/** + * The measurement, against the shipped stack on the real engine. + * + * Self-referencing roll-ups are the case worth measuring rather than assuming: + * parent and child are the same object, and the engine's summary index is keyed + * by CHILD object. All four legs were measured before the field was declared, + * and they are re-measured here on every run. + */ +describe('the roll-up actually computes, and keeps computing (#1182)', () => { + const SYS = { isSystem: true } as AnyRec; + let kernel: AnyRec; + let ql: AnyRec; + const id: Record = {}; + + const insert = async (object: string, doc: AnyRec): Promise => { + const row = await ql.insert(object, doc, { context: SYS }); + return String(row?.id ?? row?.record?.id); + }; + const rollup = async (): Promise => { + const rows = await ql.find('crm_account', { where: { id: id.parent } }, { context: SYS }); + return (rows as AnyRec[])[0]?.child_account_revenue; + }; + const account = (name: string, extra: AnyRec = {}): AnyRec => ({ + name, type: 'customer', is_active: true, ...extra, + }); + + beforeAll(async () => { + process.env.OS_REGISTRY_LOG ??= 'silent'; + kernel = new ObjectKernel({ logger: { level: 'silent' } } as never); + await kernel.use(new DefaultDatasourcePlugin({ driver: 'memory', config: {} } as never)); + await kernel.use(new MetadataPlugin({ watch: false, artifactWatch: false, environmentId: 'proj_sweep' } as never)); + await kernel.use(new ObjectQLPlugin({ environmentId: 'proj_sweep' } as never)); + // The app's own metadata is the subject. Seed data is skipped — the three + // accounts below are the whole population, so the sums are unambiguous. + await kernel.use(new AppPlugin(stack as never, undefined as never, { skipSeedData: true } as never)); + await kernel.bootstrap(); + ql = kernel.getService('objectql'); + + id.parent = await insert('crm_account', account('Global Parent')); + id.childA = await insert('crm_account', account('Child A', { parent_account: id.parent, annual_revenue: 1000 })); + id.childB = await insert('crm_account', account('Child B', { parent_account: id.parent, annual_revenue: 2500 })); + }, 120000); + + afterAll(async () => { + await kernel?.shutdown?.(); + }); + + it('sums the direct children on insert', async () => { + expect(await rollup()).toBe(3500); + }); + + it('follows a child whose revenue is edited', async () => { + await ql.update('crm_account', { id: id.childA, annual_revenue: 4000 }, { context: SYS }); + expect(await rollup()).toBe(6500); + }); + + it('drops a child that is re-parented away', async () => { + id.other = await insert('crm_account', account('Other Parent')); + await ql.update('crm_account', { id: id.childB, parent_account: id.other }, { context: SYS }); + expect(await rollup()).toBe(4000); + }); + + it('drops a child that is deleted', async () => { + await ql.delete('crm_account', { where: { id: id.childA }, context: SYS }); + expect(await rollup()).toBe(0); + }); +});