diff --git a/rmax_custom/fixtures/property_setter.json b/rmax_custom/fixtures/property_setter.json index d243bd4..3b79e0b 100644 --- a/rmax_custom/fixtures/property_setter.json +++ b/rmax_custom/fixtures/property_setter.json @@ -1,18 +1,50 @@ [ { "default_value": null, - "doc_type": "Landed Cost Item", + "doc_type": "Material Request Item", "docstatus": 0, "doctype": "Property Setter", "doctype_or_field": "DocField", - "field_name": "qty", + "field_name": "from_warehouse", "is_system_generated": 0, - "modified": "2026-03-14 15:18:50.370102", + "modified": "2026-04-07 11:59:35.070437", "module": null, - "name": "Landed Cost Item-qty-columns", - "property": "columns", - "property_type": "Int", + "name": "Material Request Item-from_warehouse-hidden", + "property": "hidden", + "property_type": "Check", "row_name": null, "value": "1" + }, + { + "default_value": null, + "doc_type": "Material Request Item", + "docstatus": 0, + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "field_name": "warehouse", + "is_system_generated": 0, + "modified": "2026-04-07 11:59:35.258029", + "module": null, + "name": "Material Request Item-warehouse-hidden", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "1" + }, + { + "default_value": null, + "doc_type": "Material Request Item", + "docstatus": 0, + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "field_name": "schedule_date", + "is_system_generated": 0, + "modified": "2026-04-08 14:12:12.985855", + "module": "", + "name": "Material Request Item-schedule_date-reqd", + "property": "reqd", + "property_type": "Check", + "row_name": null, + "value": "0" } ] \ No newline at end of file diff --git a/rmax_custom/fixtures/workflow.json b/rmax_custom/fixtures/workflow.json new file mode 100644 index 0000000..e8b0065 --- /dev/null +++ b/rmax_custom/fixtures/workflow.json @@ -0,0 +1,122 @@ +[ + { + "docstatus": 0, + "doctype": "Workflow", + "document_type": "Stock Transfer", + "is_active": 1, + "modified": "2026-04-07 17:50:07.655273", + "name": "Stock Transfer Workflow", + "override_status": 0, + "send_email_alert": 0, + "states": [ + { + "allow_edit": "Stock User", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": null, + "next_action_email_template": null, + "parent": "Stock Transfer Workflow", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 1, + "state": "Draft", + "update_field": null, + "update_value": null, + "workflow_builder_id": null + }, + { + "allow_edit": "Stock User", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": null, + "next_action_email_template": null, + "parent": "Stock Transfer Workflow", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 1, + "state": "Pending", + "update_field": null, + "update_value": null, + "workflow_builder_id": null + }, + { + "allow_edit": "Stock Manager", + "avoid_status_override": 0, + "doc_status": "1", + "is_optional_state": 0, + "message": null, + "next_action_email_template": null, + "parent": "Stock Transfer Workflow", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 1, + "state": "Approved", + "update_field": null, + "update_value": null, + "workflow_builder_id": null + }, + { + "allow_edit": "Stock Manager", + "avoid_status_override": 0, + "doc_status": "0", + "is_optional_state": 0, + "message": null, + "next_action_email_template": null, + "parent": "Stock Transfer Workflow", + "parentfield": "states", + "parenttype": "Workflow", + "send_email": 1, + "state": "Rejected", + "update_field": null, + "update_value": null, + "workflow_builder_id": null + } + ], + "transitions": [ + { + "action": "Send For Approval", + "allow_self_approval": 1, + "allowed": "Stock User", + "condition": null, + "next_state": "Pending", + "parent": "Stock Transfer Workflow", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Draft", + "workflow_builder_id": null + }, + { + "action": "Approve", + "allow_self_approval": 1, + "allowed": "Stock Manager", + "condition": null, + "next_state": "Approved", + "parent": "Stock Transfer Workflow", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending", + "workflow_builder_id": null + }, + { + "action": "Reject", + "allow_self_approval": 1, + "allowed": "Stock Manager", + "condition": null, + "next_state": "Rejected", + "parent": "Stock Transfer Workflow", + "parentfield": "transitions", + "parenttype": "Workflow", + "send_email_to_creator": 0, + "state": "Pending", + "workflow_builder_id": null + } + ], + "workflow_data": null, + "workflow_name": "Stock Transfer Workflow", + "workflow_state_field": "workflow_state" + } +] \ No newline at end of file diff --git a/rmax_custom/fixtures/workflow_action_master.json b/rmax_custom/fixtures/workflow_action_master.json new file mode 100644 index 0000000..4aae38a --- /dev/null +++ b/rmax_custom/fixtures/workflow_action_master.json @@ -0,0 +1,30 @@ +[ + { + "docstatus": 0, + "doctype": "Workflow Action Master", + "modified": "2026-02-26 12:37:52.228555", + "name": "Approve", + "workflow_action_name": "Approve" + }, + { + "docstatus": 0, + "doctype": "Workflow Action Master", + "modified": "2026-02-26 12:37:52.229648", + "name": "Reject", + "workflow_action_name": "Reject" + }, + { + "docstatus": 0, + "doctype": "Workflow Action Master", + "modified": "2026-02-26 12:37:52.230333", + "name": "Review", + "workflow_action_name": "Review" + }, + { + "docstatus": 0, + "doctype": "Workflow Action Master", + "modified": "2026-04-07 17:49:09.493280", + "name": "Send For Approval", + "workflow_action_name": "Send For Approval" + } +] \ No newline at end of file diff --git a/rmax_custom/fixtures/workflow_state.json b/rmax_custom/fixtures/workflow_state.json new file mode 100644 index 0000000..894c9b4 --- /dev/null +++ b/rmax_custom/fixtures/workflow_state.json @@ -0,0 +1,47 @@ +[ + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "remove", + "modified": "2026-02-26 12:37:52.220221", + "name": "Rejected", + "style": "Danger", + "workflow_state_name": "Rejected" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "", + "modified": "2026-04-07 17:31:52.178832", + "name": "Pending For Approval", + "style": "", + "workflow_state_name": "Pending For Approval" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "ok-sign", + "modified": "2026-02-26 12:37:52.219295", + "name": "Approved", + "style": "Success", + "workflow_state_name": "Approved" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "question-sign", + "modified": "2026-02-26 12:37:52.217960", + "name": "Pending", + "style": "", + "workflow_state_name": "Pending" + }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "", + "modified": "2026-04-07 17:15:42.323547", + "name": "Draft", + "style": "", + "workflow_state_name": "Draft" + } +] \ No newline at end of file diff --git a/rmax_custom/hooks.py b/rmax_custom/hooks.py index cf95be7..7d64532 100644 --- a/rmax_custom/hooks.py +++ b/rmax_custom/hooks.py @@ -31,8 +31,8 @@ "/assets/rmax_custom/js/sales_invoice_pos_total_popup.js", "/assets/rmax_custom/js/sales_invoice_popup.js", "/assets/rmax_custom/js/create_customer.js", - "/assets/rmax_custom/js/create_multiple_supplier.js" - + "/assets/rmax_custom/js/create_multiple_supplier.js", + "/assets/rmax_custom/js/materiel_request.js" ] @@ -257,6 +257,9 @@ # } fixtures = [ + "Workflow", + "Workflow State", + "Workflow Action Master", { "dt": "Custom Field", "filters": [ @@ -297,5 +300,46 @@ ] ] ] + }, + { + "dt": "Custom Field", + "filters": [ + [ + "name", + "in", + [ + # Sales Invoice + "Sales Invoice-custom_payment_mode", + "Sales Invoice-custom_inter_company_branch", + + # Sales Invoice Item + "Sales Invoice Item-total_vat_linewise", + + + # Quotation + "Quotation-custom_payment_mode", + + # Quotation Item + "Quotation Item-total_vat_linewise", + + + ] + ] + ] + }, + { + "dt": "Property Setter", + "filters": [ + [ + "name", + "in", + [ + "Material Request Item-warehouse-hidden", + "Material Request Item-from_warehouse-hidden", + "Material Request Item-schedule_date-reqd" + + ] + ] + ] } ] diff --git a/rmax_custom/public/js/materiel_request.js b/rmax_custom/public/js/materiel_request.js new file mode 100644 index 0000000..fe50354 --- /dev/null +++ b/rmax_custom/public/js/materiel_request.js @@ -0,0 +1,41 @@ +frappe.ui.form.on('Material Request', { + refresh: function(frm) { + if (frm.is_new()) { + set_default_target(frm); + } + }, + + material_request_type: function(frm) { + set_default_target(frm); + } + +}); + +function set_default_target(frm) { + if (!frm.doc.set_warehouse) { + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "User Permission", + filters: { + user: frappe.session.user, + allow: "Warehouse", + is_default: "1" + }, + fields: ["for_value"] + }, + callback: function(r) { + if (r.message && r.message.length > 0 && r.message[0].for_value) { + + let default_wh = r.message[0].for_value; + + frm.set_value('set_warehouse', default_wh).then(() => { + frm.refresh_field('set_warehouse'); + }); + + } + } + }); + + } +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/custom/material_request_item.json b/rmax_custom/rmax_custom/custom/material_request_item.json new file mode 100644 index 0000000..2a713c4 --- /dev/null +++ b/rmax_custom/rmax_custom/custom/material_request_item.json @@ -0,0 +1,55 @@ +{ + "custom_fields": [], + "custom_perms": [], + "doctype": "Material Request Item", + "links": [], + "property_setters": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-04-07 11:59:35.070437", + "default_value": null, + "doc_type": "Material Request Item", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "from_warehouse", + "idx": 0, + "is_system_generated": 0, + "modified": "2026-04-07 11:59:35.070437", + "modified_by": "Administrator", + "module": null, + "name": "Material Request Item-from_warehouse-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "1" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-04-07 11:59:35.258029", + "default_value": null, + "doc_type": "Material Request Item", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "warehouse", + "idx": 0, + "is_system_generated": 0, + "modified": "2026-04-07 11:59:35.258029", + "modified_by": "Administrator", + "module": null, + "name": "Material Request Item-warehouse-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "1" + } + ], + "sync_on_migrate": 0 +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer/__init__.py b/rmax_custom/rmax_custom/doctype/stock_transfer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.js b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.js new file mode 100644 index 0000000..a469bc0 --- /dev/null +++ b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.js @@ -0,0 +1,160 @@ +// Copyright (c) 2026, Enfono and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Stock Transfer', { + + onload: function(frm) { + if (frm.is_new()) { + set_default_target(frm); + } + frm.set_query('set_source_warehouse', function() { + return { + ignore_user_permissions: 1 + }; + }); + }, + + refresh: function(frm) { + if (frm.doc.__islocal && !frm.doc.__source_fixed) { + setTimeout(() => { + frm.set_value('set_source_warehouse', ''); + frm.doc.__source_fixed = true; + + }, 200); + } + }, + before_save: function(frm) { + + if (frm.doc.set_source_warehouse) { + + frappe.call({ + method: "frappe.client.get_list", + async: false, + args: { + doctype: "User Permission", + filters: { + user: frappe.session.user, + allow: "Warehouse", + for_value: frm.doc.set_source_warehouse + }, + fields: ["name"] + }, + callback: function(r) { + + if (!r.message.length) { + + frappe.call({ + method: "frappe.client.insert", + async: false, + args: { + doc: { + doctype: "User Permission", + user: frappe.session.user, + allow: "Warehouse", + for_value: frm.doc.set_source_warehouse + } + } + }); + + } + } + }); + } +} + +}); + + +function set_default_target(frm) { + + if (!frm.doc.set_target_warehouse) { + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "User Permission", + filters: { + user: frappe.session.user, + allow: "Warehouse", + is_default: 1 + }, + fields: ["for_value"], + limit: 1 + }, + callback: function(r) { + + if (r.message && r.message.length > 0) { + let default_wh = r.message[0].for_value; + frm.set_value('set_target_warehouse', default_wh); + } + + } + }); + } +} + +frappe.ui.form.on('Stock Transfer Item', { + item_code: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!row.item_code) return; + frappe.db.get_value("Item", row.item_code, "stock_uom", (r) => { + if (r && r.stock_uom) { + frappe.model.set_value(cdt, cdn, "uom", r.stock_uom); + trigger_conversion(frm, cdt, cdn); + } + }); + set_uom_query(frm, cdt, cdn); + }, + uom: function(frm, cdt, cdn) { + trigger_conversion(frm, cdt, cdn); + } +}); + + +function set_uom_query(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!row.item_code) return; + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "Item", + name: row.item_code + }, + callback: function(r) { + if (r.message && r.message.uoms) { + let uom_list = r.message.uoms.map(d => d.uom); + frm.fields_dict["items"].grid.get_field("uom").get_query = function(doc, cdt, cdn) { + return { + filters: { + name: ["in", uom_list] + } + }; + }; + + } + } + }); +} + + +function trigger_conversion(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!(row.item_code && row.uom)) return; + frappe.call({ + method: "rmax_custom.rmax_custom.doctype.stock_transfer.stock_transfer.get_item_uom_conversion", + args: { + item_code: row.item_code, + uom: row.uom + }, + callback: function(r) { + if (r.message !== undefined) { + frappe.model.set_value( + cdt, + cdn, + "uom_conversion_factor", + r.message + ); + } + } + }); +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.json b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.json new file mode 100644 index 0000000..fffbb46 --- /dev/null +++ b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.json @@ -0,0 +1,134 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:{ST}-{#####}", + "creation": "2026-04-07 12:00:51.568243", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_14du", + "series", + "material_request_type", + "column_break_vnob", + "transaction_date", + "company", + "amended_from", + "section_break_mxxb", + "set_source_warehouse", + "column_break_cpel", + "set_target_warehouse", + "section_break_mbwe", + "items" + ], + "fields": [ + { + "fieldname": "section_break_14du", + "fieldtype": "Section Break" + }, + { + "fieldname": "series", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Series", + "options": "MAT-MR-.YYYY.-", + "reqd": 1 + }, + { + "fieldname": "column_break_vnob", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "transaction_date", + "fieldtype": "Date", + "label": "Transaction Date", + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Stock Transfer", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "section_break_mxxb", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_cpel", + "fieldtype": "Column Break" + }, + { + "default": "Material Transfer", + "fieldname": "material_request_type", + "fieldtype": "Select", + "label": "Purpose", + "options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided", + "reqd": 1 + }, + { + "fieldname": "section_break_mbwe", + "fieldtype": "Section Break" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Stock Transfer Item" + }, + { + "depends_on": "eval:doc.material_request_type == 'Material Transfer'", + "fieldname": "set_source_warehouse", + "fieldtype": "Link", + "label": "Set Source Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "set_target_warehouse", + "fieldtype": "Link", + "label": "Set Target Warehouse", + "options": "Warehouse" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2026-04-08 13:56:52.609254", + "modified_by": "Administrator", + "module": "Rmax Custom", + "name": "Stock Transfer", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.py b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.py new file mode 100644 index 0000000..ac3d9b7 --- /dev/null +++ b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.py @@ -0,0 +1,64 @@ +# Copyright (c) 2026, Enfono and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils.data import get_url_to_form + + +class StockTransfer(Document): + + + + def on_submit(self): + """Run only when document is submitted (docstatus = 1)""" + if self.workflow_state != "Approved": + return + + self.create_stock_entry() + + + def create_stock_entry(self): + """Create Stock Entry for Material Transfer""" + if not self.set_target_warehouse: + frappe.throw("Target Warehouse is required") + + if not self.set_source_warehouse: + frappe.throw("Source Warehouse is required") + + if not self.items: + frappe.throw("No items found") + se = frappe.new_doc("Stock Entry") + se.stock_entry_type = "Material Transfer" + se.from_warehouse = self.set_source_warehouse + se.remarks = f"Created from Stock Transfer: {self.name}" + for item in self.items: + if item.item_code: + se.append("items", { + "item_code": item.item_code, + "qty": item.quantity, + "uom": item.uom, + "s_warehouse": self.set_source_warehouse, + "t_warehouse": self.set_target_warehouse + }) + + se.insert() + self.stock_entry = se.name + self.stock_entry_created = 1 + + frappe.msgprint( + f'Stock Entry Created: {se.name}', + alert=True, + indicator='green' + ) + + + +@frappe.whitelist() +def get_item_uom_conversion(item_code, uom): + data = frappe.db.get_value( + "UOM Conversion Detail", + {"parent": item_code, "uom": uom}, + "conversion_factor" + ) + return data or 1 \ No newline at end of file diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer/test_stock_transfer.py b/rmax_custom/rmax_custom/doctype/stock_transfer/test_stock_transfer.py new file mode 100644 index 0000000..e93ed15 --- /dev/null +++ b/rmax_custom/rmax_custom/doctype/stock_transfer/test_stock_transfer.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Enfono and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestStockTransfer(FrappeTestCase): + pass diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer_item/__init__.py b/rmax_custom/rmax_custom/doctype/stock_transfer_item/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer_item/stock_transfer_item.json b/rmax_custom/rmax_custom/doctype/stock_transfer_item/stock_transfer_item.json new file mode 100644 index 0000000..7ef154c --- /dev/null +++ b/rmax_custom/rmax_custom/doctype/stock_transfer_item/stock_transfer_item.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-04-07 16:39:44.299467", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "quantity", + "column_break_wlnz", + "stock_uom", + "uom", + "uom_conversion_factor" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fieldname": "quantity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "reqd": 1 + }, + { + "fieldname": "column_break_wlnz", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "reqd": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM", + "reqd": 1 + }, + { + "fieldname": "uom_conversion_factor", + "fieldtype": "Float", + "label": "UOM Conversion Factor", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-04-08 13:25:24.463093", + "modified_by": "Administrator", + "module": "Rmax Custom", + "name": "Stock Transfer Item", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer_item/stock_transfer_item.py b/rmax_custom/rmax_custom/doctype/stock_transfer_item/stock_transfer_item.py new file mode 100644 index 0000000..59612a6 --- /dev/null +++ b/rmax_custom/rmax_custom/doctype/stock_transfer_item/stock_transfer_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Enfono and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class StockTransferItem(Document): + pass