vendor payment and email
This commit is contained in:
greg
2025-12-03 09:47:06 -06:00
parent 721405a3db
commit f2364cf10f
9 changed files with 588 additions and 98 deletions
+100 -36
View File
@@ -4,23 +4,16 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="22810d07-7a23-46b5-943a-50479cd90088" name="Changes" comment=""> <list default="true" id="22810d07-7a23-46b5-943a-50479cd90088" name="Changes" comment="Working&#10;no payment">
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/__manifest__.py" beforeDir="false" afterPath="$PROJECT_DIR$/__manifest__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/warranty_claim.iml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/data/mail_template_warranty_claim.xml" beforeDir="false" afterPath="$PROJECT_DIR$/data/mail_template_warranty_claim.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/__init__.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/models/sale_order_warranty.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/sale_order_warranty.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/__manifest__.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/models/warranty_claim.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/warranty_claim.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/data/mail_template_warranty_claim.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/report/report_warranty_claim.xml" beforeDir="false" afterPath="$PROJECT_DIR$/report/report_warranty_claim.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/models/__init__.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/security/ir.model.access.csv" beforeDir="false" afterPath="$PROJECT_DIR$/security/ir.model.access.csv" afterDir="false" />
<change afterPath="$PROJECT_DIR$/models/sale_order_warranty.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/views/sale_order_warranty_views.xml" beforeDir="false" afterPath="$PROJECT_DIR$/views/sale_order_warranty_views.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/models/warranty_claim.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/views/warranty_claim_views.xml" beforeDir="false" afterPath="$PROJECT_DIR$/views/warranty_claim_views.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/models/warranty_vendor.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/report/report_warranty_claim.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/security/ir.model.access.csv" afterDir="false" />
<change afterPath="$PROJECT_DIR$/views/sale_order_warranty_views.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/views/warranty_claim_views.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/views/warranty_menus.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/views/warranty_vendor_views.xml" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -37,9 +30,21 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="ProjectColorInfo"><![CDATA[{ <component name="GitHubPullRequestSearchHistory">{
"associatedIndex": 3 &quot;lastFilter&quot;: {
}]]></component> &quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;ocpistol-bec&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/ocpistol-bec/warranty_claim.git&quot;,
&quot;accountId&quot;: &quot;760ad381-6204-473c-ad08-7697a29c99ce&quot;
}
}</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 3
}</component>
<component name="ProjectId" id="36DVIBY7rktVJLU8ed25ZavgFcV" /> <component name="ProjectId" id="36DVIBY7rktVJLU8ed25ZavgFcV" />
<component name="ProjectLevelVcsManager"> <component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" /> <ConfirmationsSetting value="2" id="Add" />
@@ -48,23 +53,45 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", &quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
"ModuleVcsDetector.initialDetectionPerformed": "true", &quot;Docker.odoo17/compose.yaml: Compose Deployment.executor&quot;: &quot;Run&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
"git-widget-placeholder": "master", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"node.js.detected.package.eslint": "true", &quot;git-widget-placeholder&quot;: &quot;master&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"vue.rearranger.settings.migration": "true" &quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}]]></component> }</component>
<component name="RunManager">
<configuration default="true" type="docker-deploy" factoryName="docker-compose.yml" temporary="true">
<deployment type="docker-compose.yml">
<settings />
</deployment>
<method v="2" />
</configuration>
<configuration name="odoo17/compose.yaml: Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" temporary="true" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="sourceFilePath" value="../../../../../../docker/odoo17/compose.yaml" />
</settings>
</deployment>
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Docker.odoo17/compose.yaml: Compose Deployment" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
@@ -80,10 +107,47 @@
<option name="presentableId" value="Default" /> <option name="presentableId" value="Default" />
<updated>1764541046700</updated> <updated>1764541046700</updated>
<workItem from="1764541047716" duration="3403000" /> <workItem from="1764541047716" duration="3403000" />
<workItem from="1764552643939" duration="5572000" />
<workItem from="1764602935621" duration="498000" />
<workItem from="1764604076738" duration="14528000" />
<workItem from="1764688895143" duration="7036000" />
<workItem from="1764771198296" duration="3932000" />
</task> </task>
<task id="LOCAL-00001" summary="Working&#10;no payment">
<option name="closed" value="true" />
<created>1764552691316</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1764552691316</updated>
</task>
<task id="LOCAL-00002" summary="Working&#10;no payment">
<option name="closed" value="true" />
<created>1764552756878</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1764552756878</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Working&#10;no payment" />
<option name="LAST_COMMIT_MESSAGE" value="Working&#10;no payment" />
</component>
</project> </project>
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "Warranty Claims", "name": "Warranty Claims",
"version": "17.0.1.0.0", "version": "17.0.2.0.0",
"summary": "Manage product warranty claims and vendor RMA", "summary": "Manage product warranty claims and vendor RMA",
"category": "Sales", "category": "Sales",
"author": "Wisc-Host", "author": "Wisc-Host",
@@ -18,7 +18,7 @@
"views/warranty_menus.xml", "views/warranty_menus.xml",
"views/warranty_vendor_views.xml", "views/warranty_vendor_views.xml",
"report/report_warranty_claim.xml", "report/report_warranty_claim.xml",
# "data/mail_template_warranty_claim.xml", "data/mail_template_warranty_claim.xml",
], ],
"application": True, "application": True,
"installable": True, "installable": True,
+112 -2
View File
@@ -1,6 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<data> <data>
<!-- Intentionally empty for now -->
<!-- Email template for sending a warranty claim to the vendor -->
<record id="mail_template_warranty_claim" model="mail.template">
<field name="name">Warranty Claim - Send to Vendor</field>
<!-- Link this template to the warranty claim model -->
<field name="model_id" ref="warranty_claim.model_bec_warranty_claim"/>
<!-- Subject / sender / recipient -->
<field name="subject">Warranty claim ${object.name or ''}</field>
<field name="email_from">
${(object.company_id.email_formatted or user.email_formatted) or ''}
</field>
<field name="email_to">
${(object.true_vendor_id.email or object.manufacturer_id.email) or ''}
</field>
<field name="auto_delete" eval="True"/>
<!-- Body -->
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="box-sizing:border-box;margin: 0px; padding: 0px; font-size: 13px;">
Dear
<t t-out="object.true_vendor_id.name or object.manufacturer_id.name or ''">
Vendor Name
</t>,
<br/><br/>
Please process a warranty claim / credit for the following:
<br/><br/>
<strong style="box-sizing:border-box;font-weight:bolder;">
Warranty Claim:
</strong>
<t t-out="object.name or ''">WC0001</t><br/>
<strong style="box-sizing:border-box;font-weight:bolder;">
Customer:
</strong>
<t t-out="object.partner_id.name or ''">Customer Name</t><br/>
<strong style="box-sizing:border-box;font-weight:bolder;">
True Invoice / Order Number:
</strong>
<t t-out="(object.sale_order_id.name or object.original_sale_order_id.name) or ''">
SO0001
</t><br/>
<strong style="box-sizing:border-box;font-weight:bolder;">
Warranty Type:
</strong>
<t t-out="object.warranty_type or ''">90_day</t><br/>
<br/>
<strong style="box-sizing:border-box;font-weight:bolder;">
Products and Failures:
</strong><br/>
</p>
<ul style="padding:0 0 0 32px;margin:0px 0 16px 0;box-sizing:border-box;list-style-type:disc;">
<t t-foreach="object.line_ids" t-as="line">
<li>
<strong style="box-sizing:border-box;font-weight:bolder;">Part:</strong>
<t t-out="line.product_id.default_code or line.product_id.display_name or ''">
830037
</t>
|
<strong style="box-sizing:border-box;font-weight:bolder;">Qty:</strong>
<t t-out="line.quantity or 1">1</t>
<br/>
<strong style="box-sizing:border-box;font-weight:bolder;">Model:</strong>
<t t-out="line.model or ''">Model</t>
|
<strong style="box-sizing:border-box;font-weight:bolder;">Serial:</strong>
<t t-out="line.serial_number or ''">8939263</t>
<br/>
<strong style="box-sizing:border-box;font-weight:bolder;">Failure:</strong>
<t t-out="line.failure or ''">
Does not turn on but spins freely
</t>
</li>
</t>
</ul>
<br/>
<strong style="box-sizing:border-box;font-weight:bolder;">
Date of Install:
</strong>
<t t-out="object.install_date or ''">04/24/25</t><br/>
<strong style="box-sizing:border-box;font-weight:bolder;">
Date of Failure:
</strong>
<t t-out="object.failure_date or ''">07/02/25</t><br/>
<br/>
<t t-if="not is_html_empty(object.message_partner_ids)">
Please reply to this email with your authorization or any further questions.
<br/><br/>
</t>
Thank you,<br/>
<t t-out="user.name">Your Name</t><br/>
<t t-if="user.signature" t-out="user.signature"/>
<p style="margin:0px 0 16px 0;box-sizing:border-box;"/>
</div>
</field>
</record>
</data> </data>
</odoo> </odoo>
+104 -12
View File
@@ -1,7 +1,20 @@
from odoo import api, fields, models from odoo import api, fields, models
class SaleOrder(models.Model): class SaleOrder(models.Model):
_inherit = "sale.order" _inherit = "sale.order"
# ClaimID
warranty_claim_ids = fields.One2many(
"bec.warranty.claim",
"sale_order_id",
string="Warranty Claims"
)
compressor_tag_image = fields.Binary(
string="Compressor Tag Photo",
related="warranty_claim_ids.compressor_tag_image",
readonly=True
)
# Checkbox under customer # Checkbox under customer
warranty_order = fields.Boolean(string="Warranty Order") warranty_order = fields.Boolean(string="Warranty Order")
@@ -17,6 +30,7 @@ class SaleOrder(models.Model):
[ [
("90_day", "90 Day Warranty"), ("90_day", "90 Day Warranty"),
("cabinet", "Cabinet Warranty"), ("cabinet", "Cabinet Warranty"),
("compressor", "Compressor Warranty"),
("other", "Other"), ("other", "Other"),
], ],
string="Warranty Type", string="Warranty Type",
@@ -26,7 +40,9 @@ class SaleOrder(models.Model):
warranty_true_vendor_id = fields.Many2one( warranty_true_vendor_id = fields.Many2one(
"res.partner", "res.partner",
string="True Vendor", string="True Vendor",
domain="[('supplier_rank', '>', 0)]", related="warranty_manufacturer_id.vendor_id",
store=True,
readonly=True,
) )
# Fields manufacturer requires # Fields manufacturer requires
@@ -49,11 +65,13 @@ class SaleOrder(models.Model):
store=False, store=False,
) )
@api.depends("order_line.product_id") @api.depends("order_line.product_id", "partner_id")
def _compute_warranty_original_sale_ids(self): def _compute_warranty_original_sale_ids(self):
"""Find previous sales orders for the same customer containing any """Find previous sales orders for the same customer containing any
of the products on this warranty order. of the products on this warranty order.
""" """
SaleOrder = self.env["sale.order"]
for order in self: for order in self:
if not order.partner_id or not order.order_line: if not order.partner_id or not order.order_line:
order.warranty_original_sale_ids = False order.warranty_original_sale_ids = False
@@ -64,17 +82,89 @@ class SaleOrder(models.Model):
order.warranty_original_sale_ids = False order.warranty_original_sale_ids = False
continue continue
candidates = self.search([ # Build domain safely
("id", "!=", order.id), domain = [
("partner_id", "=", order.partner_id.id), ("partner_id", "=", order.partner_id.id),
("state", "in", ["sale", "done"]), ("state", "in", ["sale", "done"]),
("order_line.product_id", "in", product_ids), ("order_line.product_id", "in", product_ids),
], limit=50) ]
# Avoid comparing with NewId_*; use the real DB id if it exists
if order._origin and order._origin.id:
domain.append(("id", "!=", order._origin.id))
candidates = SaleOrder.search(domain, limit=50)
order.warranty_original_sale_ids = candidates order.warranty_original_sale_ids = candidates
# ---------- create draft claim on SO confirm ----------
def action_confirm(self):
res = super().action_confirm()
for order in self:
if not order.warranty_order:
continue
# Avoid duplicate claims if already created (button or earlier run)
if order.warranty_claim_ids:
continue
# Build claim lines from all SO lines
line_commands = []
for so_line in order.order_line:
if not so_line.product_id:
continue
line_commands.append((0, 0, {
"sale_line_id": so_line.id,
"product_id": so_line.product_id.id,
"quantity": so_line.product_uom_qty,
# You can adjust these if later you have per-line model/serial
"model": order.warranty_model,
"serial_number": order.warranty_serial,
"failure": order.warranty_failure,
}))
claim_vals = {
"sale_order_id": order.id,
"partner_id": order.partner_id.id,
"manufacturer_id": order.warranty_manufacturer_id.id,
"true_vendor_id": order.warranty_true_vendor_id.id,
"model": order.warranty_model,
"serial_number": order.warranty_serial,
"failure": order.warranty_failure,
"warranty_type": order.warranty_type,
"original_sale_order_id": order.warranty_original_sale_id.id,
}
if line_commands:
claim_vals["line_ids"] = line_commands
self.env["bec.warranty.claim"].create(claim_vals)
return res
# ------------------------------------------------------------
def action_create_warranty_claim(self): def action_create_warranty_claim(self):
"""Button on SO warranty tab: create a warranty.claim pre-filled.""" """Button on SO warranty tab: create a warranty claim pre-filled."""
self.ensure_one() self.ensure_one()
# Build claim lines from all SO lines on this order
line_commands = []
for so_line in self.order_line:
if not so_line.product_id:
continue
line_commands.append((0, 0, {
"sale_line_id": so_line.id,
"product_id": so_line.product_id.id,
"quantity": so_line.product_uom_qty,
"model": self.warranty_model,
"serial_number": self.warranty_serial,
"failure": self.warranty_failure,
}))
claim_vals = { claim_vals = {
"sale_order_id": self.id, "sale_order_id": self.id,
"partner_id": self.partner_id.id, "partner_id": self.partner_id.id,
@@ -86,19 +176,23 @@ class SaleOrder(models.Model):
"warranty_type": self.warranty_type, "warranty_type": self.warranty_type,
"original_sale_order_id": self.warranty_original_sale_id.id, "original_sale_order_id": self.warranty_original_sale_id.id,
} }
claim = self.env["warranty.claim"].create(claim_vals)
if line_commands:
claim_vals["line_ids"] = line_commands
claim = self.env["bec.warranty.claim"].create(claim_vals)
action = self.env.ref("odoo_warranty_claims.action_warranty_claims").read()[0] action = self.env.ref("odoo_warranty_claims.action_warranty_claims").read()[0]
action["res_id"] = claim.id action["res_id"] = claim.id
action["views"] = [(self.env.ref("odoo_warranty_claims.view_warranty_claim_form").id, "form")] action["views"] = [(self.env.ref("odoo_warranty_claims.view_warranty_claim_form").id, "form")]
return action return action
def _create_invoices(self, grouped=False, final=False, date=None):
def _create_invoices(self, grouped=False, final=False, date=None):
invoices = super()._create_invoices(grouped=grouped, final=final, date=date) invoices = super()._create_invoices(grouped=grouped, final=final, date=date)
for order in self: for order in self:
if order.warranty_order: if order.warranty_order:
# Link customer invoice(s) to the new claim # Link customer invoice(s) to the new claim
claim = self.env["warranty.claim"].create({ claim = self.env["bec.warranty.claim"].create({
"sale_order_id": order.id, "sale_order_id": order.id,
"partner_id": order.partner_id.id, "partner_id": order.partner_id.id,
"manufacturer_id": order.warranty_manufacturer_id.id, "manufacturer_id": order.warranty_manufacturer_id.id,
@@ -112,5 +206,3 @@ class SaleOrder(models.Model):
}) })
# you could also auto-open or email here, but usually this is backend logic # you could also auto-open or email here, but usually this is backend logic
return invoices return invoices
+168 -9
View File
@@ -1,13 +1,22 @@
from odoo import api, fields, models from odoo import api, fields, models, _
from odoo.exceptions import UserError
class WarrantyClaim(models.Model): class WarrantyClaim(models.Model):
_name = "warranty.claim" _name = "bec.warranty.claim"
_description = "Warranty Claim" _description = "Warranty Claim"
_inherit = ["mail.thread", "mail.activity.mixin"] _inherit = ["mail.thread", "mail.activity.mixin"]
_order = "create_date desc" _order = "create_date desc"
name = fields.Char("Claim Reference", required=True, copy=False, default="New", tracking=True) name = fields.Char("Claim Reference", required=True, copy=False, default="New", tracking=True)
install_date = fields.Date("Date of Install")
failure_date = fields.Date("Date of Failure")
resolution_type = fields.Selection(
[
("credit", "Credit"),
("replacement", "Replacement"),
],
string="Requested Resolution",
)
# Link to the customer and various parties # Link to the customer and various parties
partner_id = fields.Many2one("res.partner", string="Customer", tracking=True) partner_id = fields.Many2one("res.partner", string="Customer", tracking=True)
manufacturer_id = fields.Many2one("warranty.manufacturer", string="Manufacturer") manufacturer_id = fields.Many2one("warranty.manufacturer", string="Manufacturer")
@@ -46,6 +55,19 @@ class WarrantyClaim(models.Model):
) )
# Technical / product fields # Technical / product fields
line_ids = fields.One2many(
"bec.warranty.claim.line",
"claim_id",
string="Claim Lines",
)
# customer credit (customer credit note / refund)
# customer_credit_id = fields.Many2one(
# 'account.move',
# string="Customer Credit",
# domain=[('move_type', '=', 'out_refund')],
# tracking=True,
# )
product_id = fields.Many2one("product.product", string="Product") product_id = fields.Many2one("product.product", string="Product")
model = fields.Char("Model") model = fields.Char("Model")
serial_number = fields.Char("Serial Number") serial_number = fields.Char("Serial Number")
@@ -54,6 +76,7 @@ class WarrantyClaim(models.Model):
[ [
("90_day", "90 Day Warranty"), ("90_day", "90 Day Warranty"),
("cabinet", "Cabinet Warranty"), ("cabinet", "Cabinet Warranty"),
("compressor", "Compressor Warranty"),
("other", "Other"), ("other", "Other"),
], ],
string="Warranty Type", string="Warranty Type",
@@ -63,7 +86,6 @@ class WarrantyClaim(models.Model):
[ [
("draft", "Draft"), ("draft", "Draft"),
("sent", "Sent to Vendor"), ("sent", "Sent to Vendor"),
("in_progress", "In Progress"),
("approved", "Approved"), ("approved", "Approved"),
("rejected", "Rejected"), ("rejected", "Rejected"),
("done", "Done"), ("done", "Done"),
@@ -73,6 +95,61 @@ class WarrantyClaim(models.Model):
tracking=True, tracking=True,
) )
company_id = fields.Many2one(
"res.company",
string="Company",
default=lambda self: self.env.company,
required=True,
)
compressor_tag_image = fields.Binary(
string="Compressor Tag Photo",
attachment=True,
help="Photo of the compressor nameplate/tag required for compressor warranty claims.",
)
@api.constrains("warranty_type", "compressor_tag_image")
def _check_compressor_tag_image_required(self):
for claim in self:
# adjust 'compressor' to your actual selection key
if claim.warranty_type == "compressor" and not claim.compressor_tag_image:
raise ValidationError(
_("A compressor tag photo is required for compressor warranty claims.")
)
currency_id = fields.Many2one(
"res.currency",
string="Currency",
related="company_id.currency_id",
store=True,
readonly=True,
)
vendor_move_id = fields.Many2one(
"account.move",
string="Vendor Credit / Invoice",
help="Vendor credit note or invoice used to reimburse this warranty claim.",
)
vendor_move_amount = fields.Monetary(
string="Vendor Amount",
currency_field="currency_id",
compute="_compute_vendor_move_amount",
store=False,
)
vendor_move_state = fields.Selection(
related="vendor_move_id.state",
string="Vendor Doc State",
readonly=True,
)
@api.depends("vendor_move_id")
def _compute_vendor_move_amount(self):
for claim in self:
claim.vendor_move_amount = claim.vendor_move_id.amount_total if claim.vendor_move_id else 0.0
@api.model @api.model
def create(self, vals): def create(self, vals):
if vals.get("name", "New") == "New": if vals.get("name", "New") == "New":
@@ -85,10 +162,92 @@ class WarrantyClaim(models.Model):
return super().create(vals) return super().create(vals)
def action_send_to_vendor(self): def action_send_to_vendor(self):
"""Uses an email template and moves to 'sent' state.""" """Open email wizard prefilled with the warranty template."""
self.ensure_one() self.ensure_one()
template = self.env.ref("odoo_warranty_claims.mail_template_warranty_claim", raise_if_not_found=False)
if template: # get template (adjust the XML ID if your template id is different)
template.send_mail(self.id, force_send=True) template = self.env.ref(
"warranty_claim.mail_template_warranty_claim_90day",
raise_if_not_found=False,
)
compose_form = self.env.ref(
"mail.email_compose_message_wizard_form",
raise_if_not_found=False,
)
# move to Sent state right away (optional comment this out if you prefer manual)
self.state = "sent" self.state = "sent"
return True
ctx = {
"default_model": self._name,
"default_res_ids": [self.id],
"default_use_template": bool(template),
"default_template_id": template.id if template else False,
"default_composition_mode": "comment",
"force_email": True,
}
return {
"name": _("Send Warranty Email"),
"type": "ir.actions.act_window",
"res_model": "mail.compose.message",
"view_mode": "form",
"views": [(compose_form.id, "form")],
"target": "new",
"context": ctx,
}
def action_create_vendor_credit(self):
self.ensure_one()
move = self.env["account.move"].create({
"move_type": "in_refund",
"partner_id": self.true_vendor_id.id,
"invoice_date": fields.Date.today(),
"invoice_line_ids": [
(0, 0, {
"name": f"Warranty Claim {self.name}",
"quantity": 1,
"price_unit": self.product_id.standard_price * -1, # or reimburse amount
"account_id": self.env.company.warranty_expense_account_id.id if hasattr(self.env.company, 'warranty_expense_account_id') else None,
})
],
})
self.vendor_credit_id = move.id
return {
"name": "Vendor Credit Note",
"type": "ir.actions.act_window",
"res_model": "account.move",
"view_mode": "form",
"res_id": move.id,
"target": "current",
}
class WarrantyClaimLine(models.Model):
_name = "bec.warranty.claim.line"
_description = "Warranty Claim Line"
claim_id = fields.Many2one(
"bec.warranty.claim",
string="Warranty Claim",
required=True,
ondelete="cascade",
)
sale_line_id = fields.Many2one(
"sale.order.line",
string="Sales Order Line",
)
product_id = fields.Many2one(
"product.product",
string="Product",
required=True,
)
quantity = fields.Float(
string="Quantity",
default=1.0,
)
model = fields.Char("Model")
serial_number = fields.Char("Serial Number")
failure = fields.Text("Failure Description")
+1 -1
View File
@@ -2,7 +2,7 @@
<report <report
id="action_report_warranty_claim" id="action_report_warranty_claim"
model="warranty.claim" model="bec.warranty.claim"
string="Warranty Claim" string="Warranty Claim"
report_type="qweb-pdf" report_type="qweb-pdf"
name="odoo_warranty_claims.report_warranty_claim" name="odoo_warranty_claims.report_warranty_claim"
+4 -2
View File
@@ -1,3 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_warranty_claim_user,warranty.claim.user,model_warranty_claim,base.group_user,1,1,1,1 access_bec_warranty_claim_sales_user,bec.warranty.claim.sales.user,model_bec_warranty_claim,sales_team.group_sale_salesman,1,1,1,0
access_warranty_manufacturer_admin,warranty.manufacturer.admin,model_warranty_manufacturer,base.group_system,1,1,1,1 access_bec_warranty_claim_purchase_user,bec.warranty.claim.purchase.user,model_bec_warranty_claim,purchase.group_purchase_user,1,1,1,1
access_warranty_manufacturer_purchase_user,warranty.manufacturer.purchase.user,model_warranty_manufacturer,purchase.group_purchase_user,1,1,1,1
access_bec_warranty_claim_line_user,bec.warranty.claim.line.user,model_bec_warranty_claim_line,sales_team.group_sale_salesman,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_warranty_claim_user access_bec_warranty_claim_sales_user warranty.claim.user bec.warranty.claim.sales.user model_warranty_claim model_bec_warranty_claim base.group_user sales_team.group_sale_salesman 1 1 1 1 0
3 access_warranty_manufacturer_admin access_bec_warranty_claim_purchase_user warranty.manufacturer.admin bec.warranty.claim.purchase.user model_warranty_manufacturer model_bec_warranty_claim base.group_system purchase.group_purchase_user 1 1 1 1
4 access_warranty_manufacturer_purchase_user warranty.manufacturer.purchase.user model_warranty_manufacturer purchase.group_purchase_user 1 1 1 1
5 access_bec_warranty_claim_line_user bec.warranty.claim.line.user model_bec_warranty_claim_line sales_team.group_sale_salesman 1 1 1 0
+9 -4
View File
@@ -7,17 +7,19 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<!-- Checkbox under customer --> <!-- Checkbox under customer -->
<xpath expr="//field[@name='partner_id']" position="after"> <!-- <xpath expr="//field[@name='partner_id']" position="after">-->
<field name="warranty_order"/> <!-- <field name="warranty_order"/>-->
</xpath> <!-- </xpath>-->
<!-- Add Warranty tab (always visible for now; no attrs/states) --> <!-- Add Warranty tab (always visible for now; no attrs/states) -->
<xpath expr="//page[@name='order_lines']" position="after"> <xpath expr="//page[@name='order_lines']" position="after">
<page string="Warranty"> <page string="Warranty">
<group> <group>
<group>
<field name="warranty_order"/>
</group>
<group> <group>
<field name="warranty_manufacturer_id"/> <field name="warranty_manufacturer_id"/>
<field name="warranty_true_vendor_id"/>
<field name="warranty_type"/> <field name="warranty_type"/>
</group> </group>
<group> <group>
@@ -31,6 +33,9 @@
domain="[('id', 'in', warranty_original_sale_ids)]" domain="[('id', 'in', warranty_original_sale_ids)]"
context="{'default_partner_id': partner_id}"/> context="{'default_partner_id': partner_id}"/>
</group> </group>
<group string="Compressor Tag" invisible="warranty_type != 'compressor'">
<field name="compressor_tag_image" widget="image"/>
</group>
</group> </group>
<group> <group>
<field name="warranty_failure" widget="text"/> <field name="warranty_failure" widget="text"/>
+64 -6
View File
@@ -3,8 +3,8 @@
<!-- Tree view --> <!-- Tree view -->
<record id="view_warranty_claim_tree" model="ir.ui.view"> <record id="view_warranty_claim_tree" model="ir.ui.view">
<field name="name">warranty.claim.tree</field> <field name="name">bec.warranty.claim.tree</field>
<field name="model">warranty.claim</field> <field name="model">bec.warranty.claim</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Warranty Claims"> <tree string="Warranty Claims">
<field name="name"/> <field name="name"/>
@@ -13,6 +13,7 @@
<field name="true_vendor_id"/> <field name="true_vendor_id"/>
<field name="manufacturer_id"/> <field name="manufacturer_id"/>
<field name="sale_order_id"/> <field name="sale_order_id"/>
<field name="serial_number"/>
<field name="state"/> <field name="state"/>
</tree> </tree>
</field> </field>
@@ -20,8 +21,8 @@
<!-- Form view --> <!-- Form view -->
<record id="view_warranty_claim_form" model="ir.ui.view"> <record id="view_warranty_claim_form" model="ir.ui.view">
<field name="name">warranty.claim.form</field> <field name="name">bec.warranty.claim.form</field>
<field name="model">warranty.claim</field> <field name="model">bec.warranty.claim</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Warranty Claim"> <form string="Warranty Claim">
<sheet> <sheet>
@@ -30,7 +31,9 @@
type="object" type="object"
string="Send to Vendor" string="Send to Vendor"
class="btn-primary"/> class="btn-primary"/>
<field name="state" widget="statusbar"/> <field name="state"
widget="statusbar"
statusbar_visible="draft,sent,approved,rejected,done"/>
</header> </header>
<group> <group>
@@ -48,16 +51,51 @@
<notebook> <notebook>
<page string="Product / Failure"> <page string="Product / Failure">
<!-- Lines: multiple products / failures -->
<field name="line_ids">
<tree editable="bottom">
<field name="product_id"/>
<field name="model"/>
<field name="serial_number"/>
<field name="quantity"/>
<field name="failure"/>
</tree>
<form>
<group> <group>
<field name="product_id"/> <field name="product_id"/>
<field name="quantity"/>
</group>
<group>
<field name="model"/> <field name="model"/>
<field name="serial_number"/> <field name="serial_number"/>
</group> </group>
<group> <group>
<field name="failure" widget="text"/> <field name="failure" widget="text"/>
</group> </group>
</form>
</field>
<!-- Compressor tag is on the claim header, NOT on the lines -->
<group string="Compressor Tag">
<field name="compressor_tag_image"
widget="image"
modifiers="{'invisible': [('warranty_type', '!=', 'compressor')]}"/>
</group>
</page> </page>
<page string="Vendor Reimbursement">
<group>
<group>
<field name="vendor_move_id"
context="{'default_partner_id': true_vendor_id}"
domain="[('move_type', 'in', ['in_invoice', 'in_refund']), ('partner_id', '=', true_vendor_id)]"/>
<field name="vendor_move_state"/>
</group>
<group>
<field name="vendor_move_amount" readonly="1"/>
</group>
</group>
</page>
<page string="Vendor &amp; Manufacturer"> <page string="Vendor &amp; Manufacturer">
<group> <group>
<field name="manufacturer_id"/> <field name="manufacturer_id"/>
@@ -90,10 +128,30 @@
<record id="action_warranty_claims" model="ir.actions.act_window"> <record id="action_warranty_claims" model="ir.actions.act_window">
<field name="name">Warranty Claims</field> <field name="name">Warranty Claims</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">warranty.claim</field> <field name="res_model">bec.warranty.claim</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="view_id" ref="view_warranty_claim_tree"/> <field name="view_id" ref="view_warranty_claim_tree"/>
</record> </record>
<!-- Search By Serial Number-->
<record id="view_warranty_claim_search" model="ir.ui.view">
<field name="name">bec.warranty.claim.search</field>
<field name="model">bec.warranty.claim</field>
<field name="arch" type="xml">
<search string="Search Warranty Claims">
<field name="name"/>
<field name="serial_number" string="Serial Number"/> <!-- or warranty_serial -->
<!-- optional filters -->
<filter name="my_claims"
string="My Claims"
domain="[('create_uid', '=', uid)]"/>
<filter name="state_open"
string="Open"
domain="[('state', 'in', ['draft', 'in_progress'])]"/>
</search>
</field>
</record>
</data> </data>
</odoo> </odoo>