005.1 — Model & Field Reference¶
Field-level companion to 005 — Domain Model. Where 005 stays high-level
(models that matter, how they relate), this document is the exhaustive reference: every model
in the ecommerce addon and every field it defines, with type, relation/selection values, label
and description.
Scope rule:
- Custom models (
_namestarting withecommerce.*) — every field is listed. - Extended standard Odoo models (
_inheritonres.users,sale.order,product.template, etc.) — only fields this module actually adds or redefines are listed. Native Odoo fields not touched by this module (e.g.sale.order.state,product.template.list_price) are out of scope; consult standard Odoo documentation for those. - Source:
addons_lp_ecommerce/ecommerce/models/*.py, live scan.
Contents¶
1. Custom Models¶
ecommerce.brand¶
File: models/brand.py · Inherits: mail.thread, mail.activity.mixin · Order: sequence, name, id
Product brand master data used for catalog filtering and admin CRUD.
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
name |
Char | — | Name | Brand display name | translate=True, required=True, tracking=True |
description |
Text | — | Description | Brand description text | translate=True, tracking=True |
sequence |
Integer | — | Sequence | Manual sort order for brand listing | index=True, default = current max sequence + 5 (or 10000 if none exist) |
is_active |
Boolean | — | Active Brand | Whether the brand is enabled/visible | default=True |
No _sql_constraints; name-uniqueness is only checked in api_create_brand/api_update_brand application code (case-insensitive).
ecommerce.banner¶
File: models/banner.py · Inherits: mail.thread, mail.activity.mixin, ecommerce.base.secure_model
Marketing banners shown in the customer portal, with a scheduled visibility window and optional customer/product/category targeting.
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
name |
Char | — | Name | Banner title | translate=True, required=True, tracking=True |
description |
Text | — | Description | Banner body text | — |
image |
Binary | — | Image | Banner image asset | attachment=True; served via a dedicated controller route, not the standard binary route |
activate |
Boolean | — | Active | Whether the banner is enabled | default=True |
active_datetime |
Datetime | — | Active From | Start of scheduled visibility window | — |
deactivate_datetime |
Datetime | — | Deactivate At | End of scheduled visibility window | — |
portal_company_partner_id |
Many2one | res.partner, domain is_company=True |
Customer | Restricts banner to one customer company; blank = shown to all | index=True |
promotion_url |
Char | — | Promotion URL | Target link when the banner is clicked | — |
product_categories_ids |
Many2many | product.category |
Product Categories | Categories this banner promotes | — |
products_ids |
Many2many | product.product |
Products | Products this banner promotes | — |
_get_banners_domain() defines the visibility window semantics: activate=True and
active_datetime <= now and (deactivate_datetime unset or >= now). IDs are
obfuscated externally via the ecommerce.base.secure_model mixin (see §3).
ecommerce.alert_message¶
File: models/alert_message.py · No mixins
Short alert/notice text attached to a product (product.template.alert_message_id).
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
name |
Char | — | Message | The product notice text | required=True |
Single-field model; _description = "Ecommerce Alert Message".
ecommerce.portal.user.tag¶
File: models/portal_user_tag.py · Inherits: mail.thread, mail.activity.mixin · Order: name, id
Per-company tags assignable to portal users, used for segmentation in the admin customer UI.
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
name |
Char | — | Name | Tag label | required=True, tracking=True, translate=True |
portal_company_partner_id |
Many2one | res.partner, domain is_company=True, is_b2b_portal=True |
Customer | Customer company this tag belongs to | tracking=True, ondelete='cascade' |
_sql_constraints: name_company_unique — UNIQUE(name, portal_company_partner_id) ("Tag name
must be unique per customer!"). Deleting the customer partner cascades and deletes its tags.
ecommerce.notification¶
File: models/notification.py · Inherits: mail.thread, mail.activity.mixin, ecommerce.base.secure_model · Order: date desc, id desc
In-app activity notification pushed to portal users/account managers over the bus (login events, order state changes, chat messages, user activation).
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
notif_type |
Selection | user_login, order_update, message, user_activation |
Notification Type | Category of the notification event | required=True, tracking=True |
res_model |
Char | — | Related Model | Model the notification refers to | expected: res.users or sale.order |
res_id |
Integer | — | Related Record ID | ID of the related record | — |
name |
Char | — | Label | Short notification title | required=True |
message |
Text | — | Message | Optional longer free-text body | — |
date |
Datetime | — | Date | Notification timestamp | default=now, required=True, tracking=True |
is_seen |
Boolean | — | Is Seen | Whether the recipient dismissed/read it | default=False, tracking=True |
portal_company_partner_id |
Many2one | res.partner, domain is_company=True, is_b2b_portal=True |
Customer | Customer company scope | ondelete='cascade', tracking=True |
owner_user_id |
Many2one | res.users |
User | Specific targeted user; excludes the actor from their own notifications | ondelete='cascade', tracking=True |
Overridden create() pushes each new record over bus.bus._sendone('ecommerce/notification', ...)
— the delivery mechanism behind the in-app real-time notifications described in
012 — Real-Time and Messaging. IDs are obfuscated externally
(secure_model mixin).
ecommerce.product.request.line¶
File: models/product_request_line.py · Inherits: mail.thread, mail.activity.mixin
Customer request for a product not currently in the catalog, tied to a sale order, progressing until an account manager sources/creates the product or marks it not found.
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
name |
Char | — | Name | Requested product's name/title | tracking=True |
description |
Char | — | Description | Free-text description | tracking=True |
requested_qty |
Float | — | Quantity | Quantity requested | required=True, default=1.0 |
state |
Selection | see table below | State | Fulfillment workflow status | default='submitted', tracking=True |
request_date |
Date | — | Request Date | Date the request was submitted | default=today |
in_progress_date |
Date | — | In Progress Date | Date moved to in-progress | no auto-stamping logic in this file |
added_date |
Date | — | Added Date | Date the product was added | no auto-stamping logic in this file |
not_found_date |
Date | — | Not Found Date | Date marked not-found | no auto-stamping logic in this file |
is_product_created |
Boolean | — | Is Product Created | Whether a product record was created for this request | default=False |
reference_url |
Char | — | Reference URL | Customer-supplied reference link | — |
is_b2b_portal |
Boolean | — | B2B Portal | Flags request as created via the portal | set by api_create_product_request_line |
created_product_id |
Many2one | product.product |
Created Product | Catalog product created/matched to fulfill the request | cleared automatically on transition to not_found |
sale_order_id |
Many2one | sale.order |
Sale Order | Order this request is attached to | ondelete='cascade' |
portal_company_partner_id |
Many2one | res.partner |
Customer | Customer company that made the request | — |
portal_create_uid |
Many2one | res.users |
Portal Create User | User who submitted the request | index=True, readonly=True |
portal_write_uid |
Many2one | res.users |
Portal Write User | Last user who updated the request | readonly=True; not actually populated anywhere in this file |
added_order_line_id |
Many2one | sale.order.line |
Added Order Line | Order line auto-created once the product is added | tracking=True; domain restricted to lines of sale_order_id |
state values:
| Value | Label | Meaning |
|---|---|---|
submitted |
Request Submitted | Awaiting account-manager review |
in_progress |
In Progress | Account manager sourcing/investigating |
product_added |
Product Added | Product linked/created, order line auto-generated |
not_found |
Product Not Found | Could not be sourced; closed, no product/line |
Auto-order-line logic: _auto_create_order_line() creates a sale.order.line
(product_uom_qty = requested_qty, manual=True) once state='product_added' with both
created_product_id and sale_order_id set (skipped if the order is already confirmed);
_delete_old_order_line() removes the stale line and posts a chatter note if the linked product
changes. sale_order_id.new_product_requests_count counts lines still submitted.
2. Extended Odoo Models¶
Only fields added or redefined by this module are listed. See 005 for the narrative summary of each extension.
res.users¶
File: models/res_users.py · Inherits: mail.thread, mail.activity.mixin, ecommerce.base_auto_refresh_model
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
activate |
Boolean | — | Account Enabled | Portal access, controlled by the account manager | tracking=True |
portal_activate |
Boolean | — | Portal Activate | Portal access, controlled by the customer's own company admin | tracking=True, default=True; both flags must be true to log in |
identification_no |
Char | — | National ID / Iqama | Alternate login identifier | — |
is_account_manager |
Boolean | — | Is Account Manager | Flags an internal user as a B2B account manager | default=False, index=True |
portal_company_partner_id |
Many2one | res.partner, domain is_company=True |
Customer | Company for external portal users | index=True, ondelete='restrict' |
is_b2b_portal |
Boolean | — | Is Portal User | Whether the user belongs to base.group_portal |
computed, store=True, index=1 |
is_admin_portal_user |
Boolean | — | Admin User | Flags a portal user as a company-admin sub-user | — |
otp |
Char | — | OTP | One-time-passcode value | password-reset / passwordless login |
otp_expiry_datetime |
Datetime | — | OTP Expiry | OTP expiration timestamp | — |
otp_input |
Char | — | OTP Input | Source comment: "for verify otp testing only, should delete later" | debug field |
qr_code_image |
Image | — | Invitation QR Code | QR code for user invitation | computed, non-stored |
invitation_url |
Char | — | Invitation URL | URL for user invitation | computed, non-stored |
email_state |
Selection | draft, sent, failed |
Email Status | Invitation-email send status | default='draft', tracking=True |
portal_user_code |
Char | — | Portal User Code | Auto-generated short code ({id:03d}) |
store=True |
portal_create_uid |
Many2one | res.users |
Portal Create User | Admin/internal user who created the portal user | index=True, readonly=True |
portal_write_uid |
Many2one | res.users |
Portal Write User | Intended last-portal-writer | readonly=True; never actually set |
im_status |
Char | — | IM Status | Live online/away/offline presence | computed with custom search, non-stored |
portal_user_tags |
Many2many | ecommerce.portal.user.tag |
Tags | Tags assigned to a portal user | — |
Notable behaviour:
- _compute_im_status treats stored "online"/"away" presence as stale (reports offline) once
last_poll exceeds the disconnection timer — worked around because Odoo.sh's gateway owns the
websocket and the native close handler never fires (see
012 — Real-Time and Messaging).
- create() auto-assigns base.group_portal (portal users) or
ecommerce_group_account_manager + sales/account manager groups (account managers) based on
is_b2b_portal/is_account_manager context flags.
- write() revokes all active sessions when activate or portal_activate flips False→True→False.
- OTP flow: generate_otp() (4-digit, expiry from ecommerce.otp_expiry_minutes system parameter,
default 5 min), send_otp_mail(), verify_opt(), change_password_with_otp().
- QR/invitation URLs resolve shop_portal_url from the account manager's system parameter or the
portal user's own company partner.
res.partner¶
File: models/res_partner.py
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
is_b2b_portal |
Boolean | — | B2B Portal | Marks a partner as a B2B portal company | — |
banner_ids |
One2many | ecommerce.banner via portal_company_partner_id |
Banners | Marketing banners scoped to this customer | — |
shop_portal_url |
Char | — | Shop Portal URL | Portal URL for this customer; defaults to system setting | — |
portal_order_sequence_id |
Many2one | ir.sequence |
Portal Order Sequence | Per-company order numbering sequence | auto-created per company on create() |
users_ids |
One2many | res.users via portal_company_partner_id |
Users | Portal sub-users belonging to this company | — |
account_manager_user_id |
Many2one | res.users, domain is_account_manager=True |
Account Manager User | Primary account manager | — |
portal_user_tag_ids |
One2many | ecommerce.portal.user.tag via portal_company_partner_id |
Users Tags | Tags scoped to this company | — |
property_product_pricelist |
Many2one | product.pricelist |
Pricelist | Used for sales to this partner | overrides native field: computed + inverse, ondelete='restrict' |
company_channel_id |
Many2one | discuss.channel |
Company Portal Channel | Chat/support channel with this company | — |
account_manager_ids |
Many2many | res.users, domain is_account_manager=True |
Account Managers | Secondary account managers with access | — |
Notable behaviour:
- create() defaults account_manager_user_id to the creating user and auto-provisions
portal_order_sequence_id by copying the ecommerce.seq_portal_order template.
- _get_or_create_portal_channel() idempotently creates a "{company} Support" group channel.
- _resolve_pricelist_grade_conflict() — the customer's grade_id.default_pricelist_id is
authoritative; a conflicting pricelist sent by an API caller is silently overridden and logged.
res.partner.grade¶
File: models/res_partner_grade.py
Reused as the "Customer Segment / Tier" concept (see 005 — ecommerce.tier was removed from the codebase in favor of this native model).
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
partner_ids |
One2many | res.partner via grade_id, domain is_company=True, is_b2b_portal=True |
Customers | Portal company customers assigned to this grade | — |
Full category CRUD REST payload helpers (get_categories_payload, create_category_payload,
etc.) live on this model; partners_count used in serialization is a native field, not added here.
res.config.settings¶
File: models/res_config_settings.py
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
ecommerce_shop_portal_url |
Char | — | Shop Portal URL | Default portal URL for B2B customers | config_parameter='ecommerce.shop_portal_url' |
send_email_notification |
Boolean | — | Send Email Notification | Global toggle for module notification emails | config_parameter='ecommerce.send_email_notification', default=True |
product.template¶
File: models/product_template.py
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
brand_id |
Many2one | ecommerce.brand |
Brand | Product's brand | tracking=True, ondelete='restrict' |
public_categ_ids |
Many2many | product.public.category |
Website Product Category | eCommerce categories the product is available in | ondelete='restrict'; drives shop/category filtering |
is_featured_product |
Boolean | — | Featured Product | Marks product as featured on storefront | default=False, tracking=True |
need_call |
Boolean | — | Need Call | Requires "call for price/info" instead of direct purchase | default=False, tracking=True |
product_pricelist_ids |
Many2many | product.pricelist |
Pricelists | Pricelists with specific price rules for this product | computed, non-stored |
alert_message_id |
Many2one | ecommerce.alert_message |
Product Notice | Alert/notice banner shown with this product | — |
terms |
Html | — | Terms and Conditions | Product-specific terms text | default=False |
Carries the bulk of the admin product API: listing/detail payload builders, media sync, the JSON
import pipeline (brand/category/product attribute find-or-create, product variant assignment, per-row savepoints),
and product variant generation/management (Cartesian product of product attribute values). No _sql_constraints.
product.product¶
File: models/product_product.py
No new fields — domain-building, price-computation and payload-serialization helpers for the
client "variant list mode" shop API only.
product.pricelist¶
File: models/pricelist.py
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
is_b2b_portal |
Boolean | — | B2B Portal | Flags this pricelist as portal-managed | primary filter in api_get_pricelists; also gates portal access checks in pricelist-item API |
item_count |
Integer | — | Price Rules | Number of product.pricelist.item rules attached |
computed via read_group on item_ids |
product.pricelist.item¶
File: models/pricelist_items.py
No new fields — admin/portal API CRUD for fixed-price rules only. Enforces that a portal rule is
always compute_price='fixed' (never accepted from the client) and always scoped to a template,
optionally narrowed to one variant; rejects date_start >= date_end.
product.public.category¶
File: models/public_category.py
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
is_active |
Boolean | — | Is Active | Soft-delete/visibility flag | default=True |
Exists so delete_category() can offer a soft-delete path (is_active=False) distinct from a
hard unlink() (guarded against FK violations from products still referencing the category,
returned as PUBLIC_CATEGORY_IN_USE) — native Odoo active would remove it from default
searches entirely instead.
sale.order¶
File: models/sale_order.py · Inherits: ecommerce.base_auto_refresh_model
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
is_b2b_portal |
Boolean | — | B2B Portal | Order created via the B2B portal | set by api_create_order |
portal_order_no |
Char | — | Portal Order No | Human-facing sequential order number (e.g. USR001-00042) |
readonly=True |
portal_label |
Char | — | Portal Label | User-editable display label; defaults to portal_order_no |
— |
portal_create_uid |
Many2one | res.users |
Portal Create User | Portal user who created the order | index=True, readonly=True |
portal_write_uid |
Many2one | res.users |
Portal Write User | Last portal user who wrote to the order | readonly=True; auto-set on every write() |
portal_state |
Selection | see table below | Portal State | Custom B2B order lifecycle state | default='draft', tracking=True |
portal_planned_order_date |
Date | — | Planned Order Date | Date the customer plans to place/need the order | drives portal_order_planning_state |
portal_order_purpose |
Char | — | Order Purpose | Free-text purpose/reason | — |
portal_order_planning_state |
Selection | late, on_time, upcoming |
— | Late/on-time/upcoming vs. planned date | computed, non-stored |
request_product_ids |
One2many | ecommerce.product.request.line |
Requested Products | Custom product requests on this order | — |
new_product_requests_count |
Integer | — | New Products Request | Count of request lines still submitted |
computed, non-stored |
ecommerce_attachment_count |
Integer | — | Attachments | Count of ir.attachment linked to the order |
computed, non-stored |
portal_messages_count |
Integer | — | Portal Messages Count | Messages sent from the portal | default=0, readonly=True |
portal_visible |
Boolean | — | Action Required | Unread portal message / shared request flag | default=False, readonly=True |
pricelist_id |
Many2one | product.pricelist |
Pricelist | Order pricelist | redeclares native field: store=True, precompute=True, check_company=True, tracking=1 |
portal_reviewed |
Boolean | — | Reviewed on Portal | Account manager reviewed the order | default=False |
portal_reviewed_date |
Datetime | — | Quotation Viewed Date | Timestamp of review | tracking=True |
portal_pending_submit |
Boolean | — | Pending Resubmission | Edited after quotation_submitted, not yet resubmitted |
default=False |
portal_print_quotation_date |
Datetime | — | Quotation Downloaded Date | Timestamp quotation PDF was printed | tracking=True |
account_manager_notes |
Char | — | Account Manager Notes | Internal notes, not customer-visible | — |
account_manager_comment |
Char | — | Account Manager Note | Comment visible to the customer | — |
portal_state values:
| Value | Label |
|---|---|
draft |
Draft Request |
pending_approval |
Pending Approval |
rfq_submitted |
RFQ Submitted |
rfq_updated |
RFQ Revised |
rejected |
Rejected |
quotation_submitted |
Quotation Issued |
po_submitted |
PO Submitted |
in_progress |
Processing |
delivered |
Delivered |
cancel |
Cancelled |
portal_order_planning_state values: late / on_time / upcoming. Closed states
(rfq_submitted → delivered, and rejected) always report on_time regardless of date; open
states compare portal_planned_order_date to today.
State-transition logic (see 008 — Order Lifecycle for the full
picture): api_update_order() is the central entry point — validates the target state, blocks
leaving draft/rejected/cancel with no order lines, routes quotation_submitted through
action_quotation_send() (PDF + email + resets review flags) and in_progress through
action_confirm(), otherwise maps portal_state → native state via a static table
(draft/pending_approval/rfq_submitted/rfq_updated → draft, rejected → cancel,
quotation_submitted/po_submitted → sent, delivered → sale, cancel → cancel). Toggling
portal_visible True→False calls action_done_reply(); False→True raises an
ecommerce.notification and notifies the account manager (shared-request flow). Only the
order's create_uid may unlink() it; order lines are frozen once state='sale' or
portal_state='cancel'.
sale.order.line¶
File: models/sale_order.py
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
portal_create_uid |
Many2one | res.users |
Portal Create User | Portal/admin user who created the line | readonly=True |
portal_write_uid |
Many2one | res.users |
Portal Write User | Last user who updated the line | readonly=True; set explicitly inside api_update_line, not via a model override |
portal_target_price |
Monetary | currency = currency_id |
Target Price | Price the customer is requesting/proposing | — |
manual |
Boolean | — | Manual | True when manually added vs. portal-copied | default=True |
need_call |
Boolean | related product_id.need_call |
Need Call | Product requires a phone call to order | readonly=True |
Deleting a line linked to a new product request resets that request back to state='submitted' and
clears added_order_line_id / created_product_id / is_product_created.
mail.message¶
File: models/mail_message.py
No new fields — chatter access control and portal payload serialization only:
_check_sale_order_access() (admin portal users see any/scoped order, portal users see only
their own), api_get_chatter_messages (comments only, internal notes hidden from portal),
api_post_chatter_message (bumps sale.order.portal_messages_count/portal_visible, raises an
ecommerce.notification), api_upload_chatter_attachment / api_delete_chatter_attachment.
ir.attachment¶
File: models/ir_attachment.py
| Field | Type | Relation / Selection | Label | Description | Notes |
|---|---|---|---|---|---|
is_b2b_portal |
Boolean | — | B2B Portal | Created via the B2B portal | auto-set in create() from context |
portal_create_uid |
Many2one | res.users |
Portal Create User | Uploader | index=True, readonly=True |
portal_write_uid |
Many2one | res.users |
Portal Write User | Last modifier | readonly=True |
portal_company_partner_id |
Many2one | res.partner, domain is_company=True |
Customer | Customer company | index=True, ondelete='restrict' |
auto_created |
Boolean | — | Auto Created | System-generated vs. manual upload | — |
upload_from_portal |
Boolean | — | Upload From Portal | Uploaded through the portal REST API | — |
attachment_view |
Selection | model, chatter |
Attachment View | Form vs. chatter thread association | set by upload API tag param |
_check_record_access() implements the access matrix (system admins full access; account
managers scoped to their assigned customers' orders/partners; portal users scoped to their own
company). _get_attachments_for_record() supports portal/auto-created filtering and pulls in
mail.message attachments on request.
mail.presence¶
File: models/mail_presence.py
No new fields. Implements the Odoo.sh websocket-close workaround: _cron_mark_stale_presences_offline()
flips stale online/away rows to offline past the disconnection timer (native close handler
never fires when the gateway drops the socket); _notify_online_users() suppresses a spurious
"back online" notification when a status flip follows within 20s of the previous poll (page
refresh vs. genuine reconnect).
discuss.channel¶
File: models/discuss_channel.py
No new fields. _portal_unread_count() / _portal_mark_read() track unread counts in parity with
the portal's websocket logic. message_post() mirrors new messages onto a string bus channel
(discuss.channel_<uuid>) because Odoo.sh only relays literal string channels to non-web-client
sockets — record-based channels never reach the Next.js portals.
res.country / res.country.state¶
Files: models/res_country.py, models/res_country_state.py
No new fields. api_list_countries() / api_list_states() — simple filtered/serialized listings
for portal address-form dropdowns (api_list_states requires country_id).
3. Abstract Models (Mixins)¶
ecommerce.base_abstract_model¶
File: models/_base_abstract_model.py
Empty abstract base — currently unused (no fields, no methods).
ecommerce.base_auto_refresh_model¶
File: models/_base_abstract_model.py
No fields. Broadcasts a bus.bus event on channel 'auto_refresh' ({model, id}) from
create()/write()/unlink() so open ERP Back Office list/tree views refresh live. Applied to
sale.order and res.users.
ecommerce.base.secure_model¶
File: models/_base_secure_model.py
No fields.X() declarations — exposes id_encrypted / id_external as computed properties.
Reversible Base64 + XOR ID obfuscation (ecommerce.use_encrypted_id system parameter toggle,
base64_id.secret XOR salt) so externally exposed IDs aren't raw database primary keys. Applied
to ecommerce.banner and ecommerce.notification. See
ecommerce/_docs/05_secure_model_id_obfuscation.md in the source repository
for the full mechanism. Explicitly documented as obfuscation, not cryptography — meant to be
paired with auth/ACL, not relied on alone.
Generated from a full read of every file in addons_lp_ecommerce/ecommerce/models/. If a model
gains or loses fields, regenerate this document rather than hand-editing individual rows.