summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMqdd <ahmadmiqdad27@gmail.com>2026-01-02 20:48:52 +0700
committerMqdd <ahmadmiqdad27@gmail.com>2026-01-02 20:48:52 +0700
commit486c306380f7e8dbb57f58013c69fdce6d608e1e (patch)
treecd5e601f8c065919bdbbd2ab53e12d040fa3db1f
parenta67b6e3dec66e90c59db6ffac21f6a831db938ef (diff)
parent7fdf8b3eb8cf42c7223039267faa4d22ba0ba334 (diff)
Merge branch 'main' of https://bitbucket.org/altafixco/fixco-addons
merge
-rwxr-xr-xfixco_custom/__manifest__.py1
-rwxr-xr-xfixco_custom/models/__init__.py3
-rw-r--r--fixco_custom/models/shipment_group.py101
-rwxr-xr-xfixco_custom/models/stock_picking.py17
-rw-r--r--fixco_custom/models/upload_cancel_picking.py213
-rwxr-xr-xfixco_custom/security/ir.model.access.csv2
-rw-r--r--fixco_custom/views/ir_sequence.xml11
-rwxr-xr-xfixco_custom/views/stock_picking.xml10
-rw-r--r--fixco_custom/views/upload_cancel_picking.xml61
9 files changed, 377 insertions, 42 deletions
diff --git a/fixco_custom/__manifest__.py b/fixco_custom/__manifest__.py
index 7a796ad..aef74df 100755
--- a/fixco_custom/__manifest__.py
+++ b/fixco_custom/__manifest__.py
@@ -48,6 +48,7 @@
'views/vit_kota.xml',
'views/token_log.xml',
'views/wizard_purchase_pricelist.xml',
+ 'views/upload_cancel_picking.xml',
],
'demo': [],
'css': [],
diff --git a/fixco_custom/models/__init__.py b/fixco_custom/models/__init__.py
index 37c8b45..393efeb 100755
--- a/fixco_custom/models/__init__.py
+++ b/fixco_custom/models/__init__.py
@@ -34,4 +34,5 @@ from . import coretax_faktur
from . import token_log
from . import purchase_pricelist_wizard
from . import stock_picking_return
-from . import account_move_reversal \ No newline at end of file
+from . import account_move_reversal
+from . import upload_cancel_picking \ No newline at end of file
diff --git a/fixco_custom/models/shipment_group.py b/fixco_custom/models/shipment_group.py
index d83130d..2e2dffa 100644
--- a/fixco_custom/models/shipment_group.py
+++ b/fixco_custom/models/shipment_group.py
@@ -163,47 +163,68 @@ class ShipmentGroup(models.Model):
})
def get_status(self):
- for picking_line in self.picking_lines:
- try:
- order_id = picking_line.invoice_marketplace
-
- authorization = self.sign_request()
- headers = {
- 'Content-Type': 'application/json',
- 'X-Advai-Country': 'ID',
- 'Authorization': authorization
- }
- payload = {
- "orderNumbers": [order_id],
- }
- url = "https://api.ginee.com/openapi/order/v2/list-order"
-
- response = requests.post(
- url,
- headers=headers,
- data=json.dumps(payload)
- )
+ """
+ Batch realtime check status order Ginee
+ - 1 request untuk banyak order
+ - Anti 429
+ - Siap dipanggil sebelum validate picking
+ """
+ # 1️⃣ Kumpulin invoice marketplace
+ order_map = {}
+ for line in self.picking_lines:
+ if line.invoice_marketplace:
+ order_map[line.invoice_marketplace] = line
+
+ if not order_map:
+ return
+
+ # 2️⃣ Prepare request
+ authorization = self.sign_request()
+ headers = {
+ 'Content-Type': 'application/json',
+ 'X-Advai-Country': 'ID',
+ 'Authorization': authorization
+ }
+
+ payload = {
+ "orderNumbers": list(order_map.keys())
+ }
+
+ url = "https://api.ginee.com/openapi/order/v2/list-order"
+
+ response = requests.post(
+ url,
+ headers=headers,
+ data=json.dumps(payload),
+ timeout=30
+ )
+
+ if response.status_code != 200:
+ raise UserError(_("API request failed with status code: %s") % response.status_code)
+
+ data = response.json()
+ if data.get('code') != 'SUCCESS':
+ raise UserError(_("API Error: %s - %s") % (
+ data.get('code', 'UNKNOWN'),
+ data.get('message', 'No error message')
+ ))
+
+ # 3️⃣ Mapping response ke picking line
+ contents = data.get('data', {}).get('content', [])
+ if not contents:
+ return
+
+ for item in contents:
+ order_no = item.get('orderNumber')
+ order_status = item.get('orderStatus')
+
+ picking_line = order_map.get(order_no)
+ if not picking_line:
+ continue
+
+ # Simpan status
+ picking_line.status = order_status if order_status == 'CANCELLED' else ''
- if response.status_code == 200:
- data = response.json()
- if data.get('code') == 'SUCCESS' and data.get('message') == 'OK':
- content = data.get('data', {}).get('content', [])
-
- if not content:
- raise UserError(_("No List Order information found in response"))
-
- content_info = content[0]
- if content_info.get('orderStatus') == 'CANCELLED':
- picking_line.status = content_info.get('orderStatus')
- else:
- picking_line.status = ''
- else:
- raise UserError(_("API Error: %s - %s") % (data.get('code', 'UNKNOWN'), data.get('message', 'No error message')))
- else:
- raise UserError(_("API request failed with status code: %s") % response.status_code)
-
- except Exception as e:
- raise UserError(_("Error: %s") % str(e))
def sign_request(self):
signData = '$'.join(['POST', Request_URI]) + '$'
diff --git a/fixco_custom/models/stock_picking.py b/fixco_custom/models/stock_picking.py
index 62f3d3b..4b4a850 100755
--- a/fixco_custom/models/stock_picking.py
+++ b/fixco_custom/models/stock_picking.py
@@ -66,12 +66,27 @@ class StockPicking(models.Model):
type_sku = fields.Selection([('single', 'Single SKU'), ('multi', 'Multi SKU')], string='Type SKU')
list_product = fields.Char(string='List Product')
+ def action_cancel_selected_pickings(self):
+ for picking in self:
+ if picking.state == 'done':
+ raise UserError(
+ _("Picking %s sudah DONE dan tidak bisa di-cancel.") % picking.name
+ )
+
+ if picking.state == 'assigned':
+ picking.do_unreserve()
+
+ picking.action_cancel()
+
+ return None
+
+
def rts_ginee(self):
self.get_shipping_parameter()
self.ship_order()
-
def create_invoices(self):
+
so_id = self.sale_id.id
if not so_id:
raise UserError(_("Gaada So nya!"))
diff --git a/fixco_custom/models/upload_cancel_picking.py b/fixco_custom/models/upload_cancel_picking.py
new file mode 100644
index 0000000..a42ef1d
--- /dev/null
+++ b/fixco_custom/models/upload_cancel_picking.py
@@ -0,0 +1,213 @@
+from odoo import models, fields, api, _
+from datetime import datetime
+import base64
+import xlrd
+from odoo.exceptions import ValidationError
+
+
+class UploadCancelPicking(models.Model):
+ _name = "upload.cancel.picking"
+ _description = "Upload Cancel Picking"
+ _order = "create_date desc"
+ _rec_name = "number"
+
+ picking_lines = fields.One2many(
+ 'upload.cancel.picking.line',
+ 'upload_cancel_picking_id',
+ string='Lines',
+ copy=False
+ )
+ number = fields.Char('Number', copy=False)
+ date_upload = fields.Datetime('Cancel Date', copy=False)
+ user_id = fields.Many2one(
+ 'res.users',
+ 'Created By',
+ default=lambda self: self.env.user
+ )
+ excel_file = fields.Binary('Excel File', attachment=True)
+ filename = fields.Char('File Name')
+
+ @api.model
+ def create(self, vals):
+ vals['number'] = self.env['ir.sequence'].next_by_code(
+ 'upload.cancel.picking'
+ ) or '/'
+ return super().create(vals)
+
+ def action_import_excel(self):
+ self.ensure_one()
+
+ if not self.excel_file:
+ raise ValidationError(_("Please upload an Excel file first."))
+
+ # === Load Excel ===
+ try:
+ file_content = base64.b64decode(self.excel_file)
+ workbook = xlrd.open_workbook(file_contents=file_content)
+ sheet = workbook.sheet_by_index(0)
+ except Exception:
+ raise ValidationError(_("Invalid Excel file format."))
+
+ # === Validate Header ===
+ header = [
+ str(sheet.cell(0, col).value).strip().lower()
+ for col in range(sheet.ncols)
+ ]
+
+ if 'invoice' not in header:
+ raise ValidationError(
+ _("Invalid Excel format. Expected column: Invoice")
+ )
+
+ invoice_col = header.index('invoice')
+
+ # === Read Rows ===
+ rows_data = []
+ for row_idx in range(1, sheet.nrows):
+ invoice_marketplace = str(
+ sheet.cell(row_idx, invoice_col).value
+ ).strip()
+
+ if not invoice_marketplace:
+ raise ValidationError(
+ _("Invoice kosong di baris Excel %s") % (row_idx + 1)
+ )
+
+ rows_data.append((row_idx + 1, invoice_marketplace))
+
+ if not rows_data:
+ raise ValidationError(_("Excel tidak berisi data."))
+
+ # === Validate Duplicate in Excel ===
+ seen = set()
+ duplicate_excel_rows = []
+
+ for row_num, invoice_marketplace in rows_data:
+ if invoice_marketplace in seen:
+ duplicate_excel_rows.append(str(row_num))
+ seen.add(invoice_marketplace)
+
+ if duplicate_excel_rows:
+ raise ValidationError(
+ _("Duplicate Invoice di file Excel pada baris: %s")
+ % ", ".join(duplicate_excel_rows)
+ )
+
+ # === Validate Duplicate in System ===
+ invoice_to_check = [inv for _, inv in rows_data]
+ existing_invoices = set()
+
+ chunk_size = 500
+ for i in range(0, len(invoice_to_check), chunk_size):
+ chunk = invoice_to_check[i:i + chunk_size]
+ records = self.env['upload.cancel.picking.line'].search([
+ ('invoice_marketplace', 'in', chunk)
+ ])
+ existing_invoices.update(records.mapped('invoice_marketplace'))
+
+ duplicate_system_rows = []
+ for row_num, invoice_marketplace in rows_data:
+ if invoice_marketplace in existing_invoices:
+ duplicate_system_rows.append(str(row_num))
+
+ if duplicate_system_rows:
+ raise ValidationError(
+ _("Invoice Marketplace sudah ada di sistem. "
+ "Ditemukan di baris: %s")
+ % ", ".join(duplicate_system_rows)
+ )
+
+ # === Create Lines ===
+ line_vals = [
+ (0, 0, {
+ 'invoice_marketplace': invoice_marketplace,
+ 'upload_cancel_picking_id': self.id,
+ })
+ for _, invoice_marketplace in rows_data
+ ]
+
+ # Clear old lines (safe way)
+ self.picking_lines = [(5, 0, 0)]
+ self.write({'picking_lines': line_vals})
+ self.picking_lines.get_order_id()
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Success'),
+ 'message': _('Imported %s lines from Excel.')
+ % len(line_vals),
+ 'sticky': False,
+ 'next': {'type': 'ir.actions.act_window_close'},
+ }
+ }
+
+ def action_cancel_picking(self):
+ self.date_upload = datetime.utcnow()
+ self.picking_lines.cancel_picking()
+
+
+class UploadCancelPickingLine(models.Model):
+ _name = "upload.cancel.picking.line"
+ _description = "Upload Cancel Picking Line"
+ _inherit = ['mail.thread']
+
+ upload_cancel_picking_id = fields.Many2one(
+ 'upload.cancel.picking',
+ string='Upload'
+ )
+ invoice_marketplace = fields.Char(
+ 'Invoice Marketplace',
+ required=True
+ )
+ picking_id = fields.Many2one(
+ 'stock.picking',
+ 'Picking Reference'
+ )
+ status_picking = fields.Selection([
+ ('done', 'Done'),
+ ('cancel', 'Cancel'),
+ ('assigned', 'Ready'),
+ ('confirmed', 'Waiting'),
+ ('waiting', 'Waiting Another Operation'),
+ ], related='picking_id.state')
+ message_error = fields.Text('Error Message')
+ is_grouped = fields.Boolean('Is Grouped', default=False)
+ group_key = fields.Char('Group Key')
+
+
+ def get_order_id(self):
+ StockPicking = self.env['stock.picking']
+
+ invoices = self.mapped('invoice_marketplace')
+
+ if not invoices:
+ return
+
+ # Ambil semua picking yang matching invoice_mp
+ pickings = StockPicking.search([
+ ('invoice_mp', 'in', invoices)
+ ])
+
+ picking_map = {
+ p.invoice_mp: p.id
+ for p in pickings
+ if p.invoice_mp
+ }
+
+ for line in self:
+ picking_id = picking_map.get(line.invoice_marketplace)
+
+ if picking_id:
+ line.picking_id = picking_id
+ line.message_error = False
+ else:
+ line.picking_id = False
+ line.message_error = _(
+ "Stock Picking tidak ditemukan untuk invoice %s"
+ ) % line.invoice_marketplace
+
+ def cancel_picking(self):
+ for line in self:
+ line.picking_id.action_cancel() \ No newline at end of file
diff --git a/fixco_custom/security/ir.model.access.csv b/fixco_custom/security/ir.model.access.csv
index f51c8ad..7795156 100755
--- a/fixco_custom/security/ir.model.access.csv
+++ b/fixco_custom/security/ir.model.access.csv
@@ -42,3 +42,5 @@ access_token_log,access.token.log,model_token_log,,1,1,1,1
access_purchase_pricelist_wizard,purchase.pricelist.wizard,model_purchase_pricelist_wizard,,1,1,1,1
access_stock_return_picking,stock.return.picking,model_stock_return_picking,,1,1,1,1
access_stock_return_picking_line,stock.return.picking.line,model_stock_return_picking_line,,1,1,1,1
+access_upload_cancel_picking,access.upload.cancel.picking,model_upload_cancel_picking,,1,1,1,1
+access_upload_cancel_picking_line,access.upload.cancel.picking.line,model_upload_cancel_picking_line,,1,1,1,1
diff --git a/fixco_custom/views/ir_sequence.xml b/fixco_custom/views/ir_sequence.xml
index 06e11cb..e4845f7 100644
--- a/fixco_custom/views/ir_sequence.xml
+++ b/fixco_custom/views/ir_sequence.xml
@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="0">
+ <record id="sequence_upload_cancel_picking" model="ir.sequence">
+ <field name="name">Upload Cancel Picking</field>
+ <field name="code">upload.cancel.picking</field>
+ <field name="active">TRUE</field>
+ <field name="prefix">UCP/%(year)s/</field>
+ <field name="padding">5</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ <field name="company_id">4</field>
+ </record>
+
<record id="sequence_upload_payments" model="ir.sequence">
<field name="name">Upload Payments</field>
<field name="code">upload.payments</field>
diff --git a/fixco_custom/views/stock_picking.xml b/fixco_custom/views/stock_picking.xml
index 80add71..cc1fca1 100755
--- a/fixco_custom/views/stock_picking.xml
+++ b/fixco_custom/views/stock_picking.xml
@@ -121,6 +121,16 @@
</field>
</record>
+ <record id="action_cancel_selected_pickings" model="ir.actions.server">
+ <field name="name">Cancel Picking</field>
+ <field name="model_id" ref="stock.model_stock_picking"/>
+ <field name="binding_model_id" ref="stock.model_stock_picking"/>
+ <field name="state">code</field>
+ <field name="code">
+ action = records.action_cancel_selected_pickings()
+ </field>
+ </record>
+
<record id="view_stock_picking_filter_inherit_name" model="ir.ui.view">
<field name="name">stock.picking.filter.name.extend</field>
<field name="model">stock.picking</field>
diff --git a/fixco_custom/views/upload_cancel_picking.xml b/fixco_custom/views/upload_cancel_picking.xml
new file mode 100644
index 0000000..5f14a75
--- /dev/null
+++ b/fixco_custom/views/upload_cancel_picking.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <record id="upload_cancel_picking_tree" model="ir.ui.view">
+ <field name="name">upload.cancel.picking.tree</field>
+ <field name="model">upload.cancel.picking</field>
+ <field name="arch" type="xml">
+ <tree default_order="create_date desc">
+ <field name="number"/>
+ <field name="date_upload"/>
+ <field name="user_id"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_upload_cancel_picking_form" model="ir.ui.view">
+ <field name="name">upload.cancel.picking.form</field>
+ <field name="model">upload.cancel.picking</field>
+ <field name="arch" type="xml">
+ <form string="Upload Cancel Picking">
+ <header>
+ <button name="action_import_excel" string="Import Excel" type="object" class="oe_highlight" attrs="{'invisible': [('number', '=', False)]}"/>
+ <button name="action_cancel_picking" string="Cancel Picking" type="object" class="oe_highlight" attrs="{'invisible': [('number', '=', False)]}"/>
+ <field name="number" widget="field_no_edit" options="{'no_open': True}"/>
+ <field name="date_upload"/>
+ <field name="user_id" widget="field_no_edit" options="{'no_open': True}"/>
+ </header>
+ <sheet>
+ <group>
+ <field name="excel_file" filename="filename"/>
+ <field name="filename" invisible="1"/>
+ </group>
+ <field name="picking_lines">
+ <tree editable="bottom">
+ <field name="invoice_marketplace"/>
+ <field name="picking_id"/>
+ <field name="status_picking"/>
+ <field name="message_error"/>
+ </tree>
+ </field>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="upload_cancel_picking_action" model="ir.actions.act_window">
+ <field name="name">Upload Cancel Picking</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">upload.cancel.picking</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem
+ id="menu_upload_cancel_picking"
+ name="Upload Cancel Picking"
+ parent="sale.menu_sale_report"
+ sequence="4"
+ action="upload_cancel_picking_action"
+ />
+ </data>
+</odoo>