From a2f3f75ea92d766891c3b66ccbb3b1d8cf85dd87 Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Wed, 29 Oct 2025 23:52:26 +0300 Subject: [PATCH] v3-annual --- .../odex25_annual_purchase/models/addendum.py | 52 ++++++++++-- .../models/annual_request.py | 81 +++++++++++++++---- .../models/purchase_inherit.py | 52 ++++++++++-- .../views/addendum_views.xml | 26 +++--- .../views/annual_request_views.xml | 8 ++ .../views/purchase_inherit_views.xml | 44 ++++++++-- 6 files changed, 218 insertions(+), 45 deletions(-) diff --git a/odex25_purchase/odex25_annual_purchase/models/addendum.py b/odex25_purchase/odex25_annual_purchase/models/addendum.py index 4624030c3..58834617e 100644 --- a/odex25_purchase/odex25_annual_purchase/models/addendum.py +++ b/odex25_purchase/odex25_annual_purchase/models/addendum.py @@ -9,8 +9,10 @@ class AnnualAddendum(models.Model): _order = "create_date desc" name = fields.Char(string="Reference", default=lambda self: self.env['ir.sequence'].next_by_code('odx.annual.addendum'), readonly=True, copy=False) - agreement_id = fields.Many2one('purchase.requisition', string="Agreement", required=True, domain=[('type_id.code','=','blanket_order')]) - vendor_id = fields.Many2one(related='agreement_id.vendor_id', store=True, readonly=True) + agreement_id = fields.Many2one('purchase.requisition', string="Agreement", required=True) + annual_request_id = fields.Many2one('odx.annual.request', string="Annual Request", index=True, ondelete='cascade') + vendor_id = fields.Many2one(related='annual_request_id.vendor_id', store=True, readonly=True) + department_id = fields.Many2one('hr.department', string="Department") purpose = fields.Char(string="Purpose") note = fields.Text(string="Notes") @@ -23,6 +25,9 @@ class AnnualAddendum(models.Model): ('cancel','Cancelled'), ], default='draft', tracking=True) line_ids = fields.One2many('odx.annual.addendum.line', 'addendum_id', string="Lines") + ssd_approve = fields.Boolean(string="SSD Approve", default=False) + seo_approve = fields.Boolean(string="SEO Approve", default=False) + recommendation = fields.Boolean(string="Recommendation", default=False) def _check_lines(self): for rec in self: @@ -69,6 +74,43 @@ class AnnualAddendumLine(models.Model): product_id = fields.Many2one('product.product', string="Product", required=True, domain=[('purchase_ok','=',True)]) description = fields.Char(string="Description") quantity = fields.Float(string="Quantity", default=1.0) - uom_id = fields.Many2one('uom.uom', string="UoM", related='product_id.uom_po_id', readonly=False) - price_unit = fields.Monetary(string="Unit Price", currency_field='currency_id', groups="purchase_requisition_custom.committe_member") - currency_id = fields.Many2one('res.currency', related='addendum_id.agreement_id.company_id.currency_id', readonly=True, store=True) \ No newline at end of file + uom_id = fields.Many2one('uom.uom', string="UoM", related='product_id.uom_po_id') + price_unit = fields.Monetary(string="Unit Price", currency_field='currency_id') + currency_id = fields.Many2one( + 'res.currency', + related='addendum_id.agreement_id.company_id.currency_id', + readonly=True, store=True + ) + addendum_company_id = fields.Many2one( + 'res.company', + related='addendum_id.agreement_id.company_id', + readonly=True, store=True + ) + taxes_id = fields.Many2many( + 'account.tax', string="Taxes", + domain="[('type_tax_use','=','purchase'), ('company_id','=', addendum_company_id)]" + ) + price_subtotal = fields.Monetary( + string="Subtotal", currency_field='currency_id', + compute='_compute_amounts', store=True, readonly=True + ) + + @api.depends('quantity', 'price_unit', 'taxes_id', 'currency_id', 'product_id') + def _compute_amounts(self): + for rec in self: + qty = rec.quantity or 0.0 + price_unit = rec.price_unit or 0.0 + currency = rec.currency_id + + taxes = rec.taxes_id + if rec.addendum_company_id: + taxes = taxes.filtered(lambda t: t.company_id == rec.addendum_company_id) + + res = taxes.compute_all( + price_unit, + currency=currency, + quantity=qty, + product=rec.product_id, + partner=None + ) + rec.price_subtotal = res['total_excluded'] diff --git a/odex25_purchase/odex25_annual_purchase/models/annual_request.py b/odex25_purchase/odex25_annual_purchase/models/annual_request.py index 8a5679f1c..4059311f6 100644 --- a/odex25_purchase/odex25_annual_purchase/models/annual_request.py +++ b/odex25_purchase/odex25_annual_purchase/models/annual_request.py @@ -35,8 +35,8 @@ class AnnualPurchaseRequest(models.Model): note = fields.Text(string="Notes") state = fields.Selection(selection=STATES, default='draft', tracking=True) line_ids = fields.One2many('odx.annual.request.line', 'request_id', string="Products") - agreement_id = fields.Many2one('purchase.requisition', string="Purchase Agreement", readonly=True, tracking=True) - vendor_id = fields.Many2one('res.partner', string="Selected Vendor", domain=[('supplier_rank','>',0)], tracking=True) + agreement_id = fields.Many2one('purchase.requisition', string="Purchase Agreement", readonly=True) + vendor_id = fields.Many2one('res.partner', string="Selected Vendor", domain=[('supplier_rank','>',0)]) currency_id = fields.Many2one('res.currency', string="Currency", default=lambda self: self.env.company.currency_id.id) technical_notes = fields.Html(string="Technical Notes") financial_notes = fields.Html(string="Financial Notes", groups="purchase_requisition_custom.committe_member") @@ -46,13 +46,13 @@ class AnnualPurchaseRequest(models.Model): ('rejected','Rejected by Committee'), ('recommended','Recommended'), ('approved','Approved by Committee'), - ], default='none', tracking=True) + ], default='none') product_category_ids = fields.Many2many('product.category', string='Items Categories', compute='_compute_product_category_ids', store=True) - committee_enabled = fields.Boolean(string="Require Committee Review", default=False, tracking=True) + committee_enabled = fields.Boolean(string="Require Committee Review", default=False) ssd_approve = fields.Boolean(string="SSD Approve", default=False) seo_approve = fields.Boolean(string="SEO Approve", default=False) @@ -78,33 +78,71 @@ class AnnualPurchaseRequest(models.Model): store=False, readonly=True, ) + addendum_count = fields.Integer(string='Addendums', compute='_compute_addendum_count', readonly=True) + def _compute_addendum_count(self): + Addendum = self.env['odx.annual.addendum'] + for rec in self: + rec.addendum_count = Addendum.search_count([('annual_request_id', '=', rec.id)]) + def action_open_addendums(self): + self.ensure_one() + domain = [('annual_request_id', '=', self.id)] + return { + 'name': _('Addendums'), + 'type': 'ir.actions.act_window', + 'res_model': 'odx.annual.addendum', + 'view_mode': 'tree,form', + 'domain': domain, + 'context': { + 'default_annual_request_id': self.id, + 'default_agreement_id': self.agreement_id.id if self.agreement_id else False, + 'default_department_id': self.department_id.id if self.department_id else False, + 'default_purpose': self.purpose or False, + }, + 'target': 'current', + } def _compute_attach_no(self): Attachment = self.env['ir.attachment'] for rec in self: - rec.attach_no = Attachment.search_count([ + count_self = Attachment.search_count([ ('res_model', '=', rec._name), ('res_id', '=', rec.id) ]) + purchase_orders = self.env['purchase.order'].search([('annual_request_id', '=', rec.id)]) + count_po = 0 + if purchase_orders: + count_po = Attachment.search_count([ + ('res_model', '=', 'purchase.order'), + ('res_id', 'in', purchase_orders.ids) + ]) + + rec.attach_no = count_self + count_po + def get_attachments(self): self.ensure_one() + Attachment = self.env['ir.attachment'] + + purchase_orders = self.env['purchase.order'].search([('annual_request_id', '=', self.id)]) + + domain = ['|', + '&', ('res_model', '=', self._name), ('res_id', 'in', self.ids), + '&', ('res_model', '=', 'purchase.order'), ('res_id', 'in', purchase_orders.ids) + ] + return { - 'name': "Documents", + 'name': _("Documents"), 'type': 'ir.actions.act_window', 'res_model': 'ir.attachment', 'view_mode': 'kanban,tree,form', - 'domain': [ - ('res_model', '=', self._name), - ('res_id', 'in', self.ids) - ], + 'domain': domain, 'context': { 'default_res_model': self._name, 'default_res_id': self.id, - }, 'target': 'current', } + def copy(self, default=None): default = dict(default or {}) @@ -219,6 +257,7 @@ class AnnualPurchaseRequest(models.Model): self._check_lines() order_line_vals = [] for line in self.line_ids: + line.sudo() order_line_vals.append((0, 0, { 'product_id': line.product_id.id, 'name': line.description or line.product_id.description_purchase or line.product_id.name, @@ -294,13 +333,21 @@ class AnnualPurchaseRequest(models.Model): if manager.user_id.id == rec.env.uid : rec.write({'state': 'procurement'}) else: - raise Warning(_("Sorry, The Approval For The Direct Manager '%s' Only !")%(rec.employee_id.parent_id.name)) + raise UserError(_("Sorry, The Approval For The Direct Manager '%s' Only !")%(rec.employee_id.parent_id.name)) else: rec.write({'state': 'procurement'}) def action_manager_reject(self, reason=False): - self.message_post(body=_("Rejected by Manager: %s") % (reason or '')) - self.write({'state': 'rejected'}) + for rec in self: + manager = rec.sudo().employee_id.parent_id + if manager: + if manager.user_id.id == rec.env.uid : + rec.write({'state': 'rejected'}) + else: + raise UserError(_("Sorry, The Approval For The Direct Manager '%s' Only !")%(rec.employee_id.parent_id.name)) + else: + rec.write({'state': 'rejected'}) + def action_send_to_committee(self): self.write({'sent_to_commitee': True}) @@ -381,10 +428,10 @@ class AnnualPurchaseRequestLine(models.Model): _description = "Annual Purchase Request Line" request_id = fields.Many2one('odx.annual.request', string="Request", required=True, ondelete='cascade') - product_id = fields.Many2one('product.product', string="Product", required=True, domain=[('purchase_ok','=',True)]) + product_id = fields.Many2one('product.product', string="Product", required=True) description = fields.Char(string="Technical Description") quantity = fields.Float(string="Quantity", default=1.0) - uom_id = fields.Many2one('uom.uom', string="UoM", related='product_id.uom_po_id', readonly=False) - price_unit = fields.Monetary(string="Unit Price", currency_field='currency_id', groups="purchase_requisition_custom.committe_member") + uom_id = fields.Many2one('uom.uom', string="UoM", related='product_id.uom_po_id') + price_unit = fields.Monetary(string="Unit Price", currency_field='currency_id') currency_id = fields.Many2one('res.currency', related='request_id.currency_id', readonly=True, store=True) technical_spec = fields.Text(string="Technical Specification") \ No newline at end of file diff --git a/odex25_purchase/odex25_annual_purchase/models/purchase_inherit.py b/odex25_purchase/odex25_annual_purchase/models/purchase_inherit.py index 3e31eda36..431d2cf6c 100644 --- a/odex25_purchase/odex25_annual_purchase/models/purchase_inherit.py +++ b/odex25_purchase/odex25_annual_purchase/models/purchase_inherit.py @@ -9,13 +9,16 @@ class PurchaseOrder(models.Model): department_id = fields.Many2one('hr.department', string="Department") purpose = fields.Char(string="Purpose") is_recommended = fields.Boolean(string="Recommended by Committee") - - # حقول محسوبة من الاحتياج السنوي - annual_purchase_commitee = fields.Boolean( - string='Annual Committee?', - compute='_compute_annual_committee_fields', - store=True + technical_attachment_id = fields.Many2one( + 'ir.attachment', + string="Technical Offer Attachment", ) + # حقول محسوبة من الاحتياج السنوي + # annual_purchase_commitee = fields.Boolean( + # string='Annual Committee?', + # compute='_compute_annual_committee_fields', + # store=True + # ) annual_can_committee_vote = fields.Boolean( compute='_compute_annual_can_committee_vote' ) @@ -24,7 +27,20 @@ class PurchaseOrder(models.Model): string='Is Technical Committee', store=False ) + show_send_purchase_manager = fields.Boolean(compute='_compute_show_send_purchase_manager') + cancel_reason = fields.Text(string="Cancel Reason") + @api.depends('state', 'requisition_state', 'requisition_type_exclusive','requisition_id', 'is_purchase_budget') + def _compute_show_send_purchase_manager(self): + for record in self: + if ( + (record.state == 'sent' and not record.is_purchase_budget and not record.requisition_id) + or + (record.state == 'wait' and not record.is_purchase_budget and not record.requisition_id) + ): + record.show_send_purchase_manager = False # يظهر الزر + else: + record.show_send_purchase_manager = True # يخفي الزر @api.depends( 'requisition_id', 'requisition_id.state', 'requisition_id.purchase_commitee', @@ -168,7 +184,30 @@ class PurchaseOrder(models.Model): 'context': {'default_order_id': self.id} } + def _fill_annual_prices_from_po(self): + for po in self: + annual = po.annual_request_id + if not annual: + continue + + for rline in annual.line_ids: + pol = po.order_line.filtered(lambda l: l.product_id == rline.product_id)[:1] + if not pol: + continue + + price = pol.price_unit + + if po.currency_id != annual.currency_id: + convert_date = po.date_order.date() if po.date_order else fields.Date.context_today(self) + price = po.currency_id._convert( + price, annual.currency_id, po.company_id, convert_date + ) + + if pol.product_uom and rline.uom_id and pol.product_uom != rline.uom_id: + price = pol.product_uom._compute_price(price, rline.uom_id) + + rline.price_unit = price def action_sign(self): if self.annual_request_id and self.annual_request_id.committee_enabled: @@ -187,6 +226,7 @@ class PurchaseOrder(models.Model): for order in other_orders: order.action_unsign() self.annual_request_id.vendor_id = self.partner_id.id + self._fill_annual_prices_from_po() if self.annual_request_id.ssd_approve: self.annual_request_id.state = 'ssd' elif self.annual_request_id.seo_approve: diff --git a/odex25_purchase/odex25_annual_purchase/views/addendum_views.xml b/odex25_purchase/odex25_annual_purchase/views/addendum_views.xml index c6b34b270..93f4c7732 100644 --- a/odex25_purchase/odex25_annual_purchase/views/addendum_views.xml +++ b/odex25_purchase/odex25_annual_purchase/views/addendum_views.xml @@ -27,16 +27,25 @@ + + + + + +
@@ -104,6 +110,8 @@ + diff --git a/odex25_purchase/odex25_annual_purchase/views/purchase_inherit_views.xml b/odex25_purchase/odex25_annual_purchase/views/purchase_inherit_views.xml index 456d68c7a..68ffaab04 100644 --- a/odex25_purchase/odex25_annual_purchase/views/purchase_inherit_views.xml +++ b/odex25_purchase/odex25_annual_purchase/views/purchase_inherit_views.xml @@ -145,6 +145,9 @@ + + + @@ -324,11 +327,6 @@ -
- - - -
@@ -337,4 +335,40 @@ + + + purchase.order.form.custom + purchase.order + + + + + + + + + + + +