Merge branch 'develop' into bank-trans-party-automatch

This commit is contained in:
Marica
2023-06-06 19:03:26 +05:30
committed by GitHub
140 changed files with 8643 additions and 6495 deletions

View File

@@ -38,6 +38,7 @@ def make_closing_entries(closing_entries, voucher_name):
"closing_date": closing_date, "closing_date": closing_date,
} }
) )
cle.flags.ignore_permissions = True
cle.submit() cle.submit()

View File

@@ -21,8 +21,6 @@
"allow_multi_currency_invoices_against_single_party_account", "allow_multi_currency_invoices_against_single_party_account",
"journals_section", "journals_section",
"merge_similar_account_heads", "merge_similar_account_heads",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
"column_break_18", "column_break_18",
@@ -176,13 +174,6 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Stale Days" "label": "Stale Days"
}, },
{
"default": "0",
"description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow",
"fieldtype": "Check",
"label": "Enable Custom Cash Flow Format"
},
{ {
"default": "0", "default": "0",
"description": "Payment Terms from orders will be fetched into the invoices as is", "description": "Payment Terms from orders will be fetched into the invoices as is",
@@ -341,11 +332,6 @@
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "POS" "label": "POS"
}, },
{
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
},
{ {
"default": "0", "default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
@@ -420,7 +406,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-05-17 12:20:04.107641", "modified": "2023-06-01 15:42:44.912316",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -1,6 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapper', {
});

View File

@@ -1,275 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:section_name",
"beta": 0,
"creation": "2018-02-08 10:00:14.066519",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Section Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_header",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Header",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "e.g Adjustments for:",
"fieldname": "section_leader",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Leader",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_subtotal",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Subtotal",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_footer",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Footer",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"options": "Cash Flow Mapping Template Details",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "position",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Position",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-15 18:28:55.034933",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapper",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "name",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMapper(Document):
pass

View File

@@ -1,25 +0,0 @@
DEFAULT_MAPPERS = [
{
"doctype": "Cash Flow Mapper",
"section_footer": "Net cash generated by operating activities",
"section_header": "Cash flows from operating activities",
"section_leader": "Adjustments for",
"section_name": "Operating Activities",
"position": 0,
"section_subtotal": "Cash generated from operations",
},
{
"doctype": "Cash Flow Mapper",
"position": 1,
"section_footer": "Net cash used in investing activities",
"section_header": "Cash flows from investing activities",
"section_name": "Investing Activities",
},
{
"doctype": "Cash Flow Mapper",
"position": 2,
"section_footer": "Net cash used in financing activites",
"section_header": "Cash flows from financing activities",
"section_name": "Financing Activities",
},
]

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestCashFlowMapper(unittest.TestCase):
pass

View File

@@ -1,43 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapping', {
refresh: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
reset_check_fields: function(frm) {
frm.fields.filter(field => field.df.fieldtype === 'Check')
.map(field => frm.set_df_property(field.df.fieldname, 'read_only', 0));
},
has_checked_field(frm) {
const val = frm.fields.filter(field => field.value === 1);
return val.length ? 1 : 0;
},
_disable_unchecked_fields: function(frm) {
// get value of clicked field
frm.fields.filter(field => field.value === 0)
.map(field => frm.set_df_property(field.df.fieldname, 'read_only', 1));
},
disable_unchecked_fields: function(frm) {
frm.events.reset_check_fields(frm);
const checked = frm.events.has_checked_field(frm);
if (checked) {
frm.events._disable_unchecked_fields(frm);
}
},
is_working_capital: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_finance_cost: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_income_tax_liability: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_income_tax_expense: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_finance_cost_adjustment: function(frm) {
frm.events.disable_unchecked_fields(frm);
}
});

View File

@@ -1,359 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:mapping_name",
"beta": 0,
"creation": "2018-02-08 09:28:44.678364",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "mapping_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "label",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Label",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"options": "Cash Flow Mapping Accounts",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sb_1",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Select Maximum Of 1",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_finance_cost",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Finance Cost",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_working_capital",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Working Capital",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_finance_cost_adjustment",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Finance Cost Adjustment",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_income_tax_liability",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Income Tax Liability",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_income_tax_expense",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Income Tax Expense",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-15 08:25:18.693533",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapping",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "name",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -1,22 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class CashFlowMapping(Document):
def validate(self):
self.validate_checked_options()
def validate_checked_options(self):
checked_fields = [
d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1
]
if len(checked_fields) > 1:
frappe.throw(
_("You can only select a maximum of one option from the list of check boxes."),
title=_("Error"),
)

View File

@@ -1,28 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
class TestCashFlowMapping(unittest.TestCase):
def setUp(self):
if frappe.db.exists("Cash Flow Mapping", "Test Mapping"):
frappe.delete_doc("Cash Flow Mappping", "Test Mapping")
def tearDown(self):
frappe.delete_doc("Cash Flow Mapping", "Test Mapping")
def test_multiple_selections_not_allowed(self):
doc = frappe.new_doc("Cash Flow Mapping")
doc.mapping_name = "Test Mapping"
doc.label = "Test label"
doc.append("accounts", {"account": "Accounts Receivable - _TC"})
doc.is_working_capital = 1
doc.is_finance_cost = 1
self.assertRaises(frappe.ValidationError, doc.insert)
doc.is_finance_cost = 0
doc.insert()

View File

@@ -1,73 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:account",
"beta": 0,
"creation": "2018-02-08 09:25:34.353995",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-02-08 09:25:34.353995",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapping Accounts",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMappingAccounts(Document):
pass

View File

@@ -1,6 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapping Template', {
});

View File

@@ -1,123 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-02-08 10:20:18.316801",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "template_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Template Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "mapping",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Cash Flow Mapping",
"length": 0,
"no_copy": 0,
"options": "Cash Flow Mapping Template Details",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-08 10:20:18.316801",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapping Template",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMappingTemplate(Document):
pass

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestCashFlowMappingTemplate(unittest.TestCase):
pass

View File

@@ -1,6 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapping Template Details', {
});

View File

@@ -1,34 +0,0 @@
{
"actions": [],
"creation": "2018-02-08 10:18:48.513608",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"mapping"
],
"fields": [
{
"fieldname": "mapping",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Mapping",
"options": "Cash Flow Mapping",
"reqd": 1,
"unique": 1
}
],
"istable": 1,
"links": [],
"modified": "2022-02-21 03:34:57.902332",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapping Template Details",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMappingTemplateDetails(Document):
pass

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestCashFlowMappingTemplateDetails(unittest.TestCase):
pass

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.query_builder.functions import IfNull, Sum from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
@@ -16,12 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_multi_mode_option, update_multi_mode_option,
) )
from erpnext.accounts.party import get_due_date, get_party_account from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.serial_no.serial_no import (
get_delivered_serial_nos,
get_pos_reserved_serial_nos,
get_serial_nos,
)
class POSInvoice(SalesInvoice): class POSInvoice(SalesInvoice):
@@ -71,6 +66,7 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points() self.apply_loyalty_points()
self.check_phone_payments() self.check_phone_payments()
self.set_status(update=True) self.set_status(update=True)
self.submit_serial_batch_bundle()
if self.coupon_code: if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
@@ -112,6 +108,29 @@ class POSInvoice(SalesInvoice):
update_coupon_code_count(self.coupon_code, "cancelled") update_coupon_code_count(self.coupon_code, "cancelled")
self.delink_serial_and_batch_bundle()
def delink_serial_and_batch_bundle(self):
for row in self.items:
if row.serial_and_batch_bundle:
if not self.consolidated_invoice:
frappe.db.set_value(
"Serial and Batch Bundle",
row.serial_and_batch_bundle,
{"is_cancelled": 1, "voucher_no": ""},
)
row.db_set("serial_and_batch_bundle", None)
def submit_serial_batch_bundle(self):
for item in self.items:
if item.serial_and_batch_bundle:
doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
if doc.docstatus == 0:
doc.flags.ignore_voucher_validation = True
doc.submit()
def check_phone_payments(self): def check_phone_payments(self):
for pay in self.payments: for pay in self.payments:
if pay.type == "Phone" and pay.amount >= 0: if pay.type == "Phone" and pay.amount >= 0:
@@ -129,88 +148,6 @@ class POSInvoice(SalesInvoice):
if paid_amt and pay.amount != paid_amt: if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_pos_reserved_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
filters = {"item_code": item.item_code, "warehouse": item.warehouse}
if item.batch_no:
filters["batch_no"] = item.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(", ".join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
frappe.throw(
_(
"Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no."
).format(item.idx, bold_invalid_serial_nos),
title=_("Item Unavailable"),
)
elif invalid_serial_nos:
frappe.throw(
_(
"Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no."
).format(item.idx, bold_invalid_serial_nos),
title=_("Item Unavailable"),
)
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(
abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
)
bold_invalid_batch_no = frappe.bold(item.batch_no)
if (available_batch_qty - reserved_batch_qty) == 0:
frappe.throw(
_(
"Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no."
).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"),
)
elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
frappe.throw(
_(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
).format(
item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed
),
title=_("Item Unavailable"),
)
def validate_delivered_serial_nos(self, item):
delivered_serial_nos = get_delivered_serial_nos(item.serial_no)
if delivered_serial_nos:
bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos))
frappe.throw(
_(
"Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no."
).format(item.idx, bold_delivered_serial_nos),
title=_("Item Unavailable"),
)
def validate_invalid_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
error_msg = []
invalid_serials, msg = "", ""
for serial_no in serial_nos:
if not frappe.db.exists("Serial No", serial_no):
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
msg = _("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(
item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)
)
if invalid_serials:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self): def validate_stock_availablility(self):
if self.is_return: if self.is_return:
return return
@@ -223,13 +160,7 @@ class POSInvoice(SalesInvoice):
from erpnext.stock.stock_ledger import is_negative_stock_allowed from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"): for d in self.get("items"):
if d.serial_no: if not d.serial_and_batch_bundle:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
self.validate_invalid_serial_nos(d)
elif d.batch_no:
self.validate_pos_reserved_batch_qty(d)
else:
if is_negative_stock_allowed(item_code=d.item_code): if is_negative_stock_allowed(item_code=d.item_code):
return return
@@ -258,36 +189,15 @@ class POSInvoice(SalesInvoice):
def validate_serialised_or_batched_item(self): def validate_serialised_or_batched_item(self):
error_msg = [] error_msg = []
for d in self.get("items"): for d in self.get("items"):
serialized = d.get("has_serial_no") error_msg = ""
batched = d.get("has_batch_no") if d.get("has_serial_no") and not d.serial_and_batch_bundle:
no_serial_selected = not d.get("serial_no") error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}"
no_batch_selected = not d.get("batch_no")
msg = "" elif d.get("has_batch_no") and not d.serial_and_batch_bundle:
item_code = frappe.bold(d.item_code) error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
serial_nos = get_serial_nos(d.serial_no)
if serialized and batched and (no_batch_selected or no_serial_selected):
msg = _(
"Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction."
).format(d.idx, item_code)
elif serialized and no_serial_selected:
msg = _(
"Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction."
).format(d.idx, item_code)
elif batched and no_batch_selected:
msg = _(
"Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction."
).format(d.idx, item_code)
elif serialized and not no_serial_selected and len(serial_nos) != d.qty:
msg = _("Row #{}: You must select {} serial numbers for item {}.").format(
d.idx, frappe.bold(cint(d.qty)), item_code
)
if msg:
error_msg.append(msg)
if error_msg: if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) frappe.throw(error_msg, title=_("Serial / Batch Bundle Missing"), as_list=True)
def validate_return_items_qty(self): def validate_return_items_qty(self):
if not self.get("is_return"): if not self.get("is_return"):
@@ -652,7 +562,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.stock_qty max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles and frappe.get_value( if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item" "Item", item.item_code, "is_stock_item"
): ):

View File

@@ -5,12 +5,18 @@ import copy
import unittest import unittest
import frappe import frappe
from frappe import _
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -249,7 +255,7 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
) )
serial_nos = get_serial_nos(se.get("items")[0].serial_no) serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
pos = create_pos_invoice( pos = create_pos_invoice(
company="_Test Company", company="_Test Company",
@@ -260,11 +266,11 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
serial_no=[serial_nos[0]],
rate=1000, rate=1000,
do_not_save=1, do_not_save=1,
) )
pos.get("items")[0].serial_no = serial_nos[0]
pos.append( pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
) )
@@ -276,7 +282,9 @@ class TestPOSInvoice(unittest.TestCase):
pos_return.insert() pos_return.insert()
pos_return.submit() pos_return.submit()
self.assertEqual(pos_return.get("items")[0].serial_no, serial_nos[0]) self.assertEqual(
get_serial_nos_from_bundle(pos_return.get("items")[0].serial_and_batch_bundle)[0], serial_nos[0]
)
def test_partial_pos_returns(self): def test_partial_pos_returns(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -289,7 +297,7 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
) )
serial_nos = get_serial_nos(se.get("items")[0].serial_no) serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
pos = create_pos_invoice( pos = create_pos_invoice(
company="_Test Company", company="_Test Company",
@@ -300,12 +308,12 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
serial_no=serial_nos,
qty=2, qty=2,
rate=1000, rate=1000,
do_not_save=1, do_not_save=1,
) )
pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1]
pos.append( pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
) )
@@ -317,14 +325,27 @@ class TestPOSInvoice(unittest.TestCase):
# partial return 1 # partial return 1
pos_return1.get("items")[0].qty = -1 pos_return1.get("items")[0].qty = -1
pos_return1.get("items")[0].serial_no = serial_nos[0]
bundle_id = frappe.get_doc(
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
)
bundle_id.remove(bundle_id.entries[1])
bundle_id.save()
bundle_id.load_from_db()
serial_no = bundle_id.entries[0].serial_no
self.assertEqual(serial_no, serial_nos[0])
pos_return1.insert() pos_return1.insert()
pos_return1.submit() pos_return1.submit()
# partial return 2 # partial return 2
pos_return2 = make_sales_return(pos.name) pos_return2 = make_sales_return(pos.name)
self.assertEqual(pos_return2.get("items")[0].qty, -1) self.assertEqual(pos_return2.get("items")[0].qty, -1)
self.assertEqual(pos_return2.get("items")[0].serial_no, serial_nos[1]) serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
self.assertEqual(serial_no, serial_nos[1])
def test_pos_change_amount(self): def test_pos_change_amount(self):
pos = create_pos_invoice( pos = create_pos_invoice(
@@ -368,7 +389,7 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
) )
serial_nos = get_serial_nos(se.get("items")[0].serial_no) serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
pos = create_pos_invoice( pos = create_pos_invoice(
company="_Test Company", company="_Test Company",
@@ -380,10 +401,10 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
rate=1000, rate=1000,
serial_no=[serial_nos[0]],
do_not_save=1, do_not_save=1,
) )
pos.get("items")[0].serial_no = serial_nos[0]
pos.append( pos.append(
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
) )
@@ -401,10 +422,10 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
rate=1000, rate=1000,
serial_no=[serial_nos[0]],
do_not_save=1, do_not_save=1,
) )
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append( pos2.append(
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
) )
@@ -423,7 +444,7 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
) )
serial_nos = get_serial_nos(se.get("items")[0].serial_no) serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
si = create_sales_invoice( si = create_sales_invoice(
company="_Test Company", company="_Test Company",
@@ -435,11 +456,11 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
rate=1000, rate=1000,
update_stock=1,
serial_no=[serial_nos[0]],
do_not_save=1, do_not_save=1,
) )
si.get("items")[0].serial_no = serial_nos[0]
si.update_stock = 1
si.insert() si.insert()
si.submit() si.submit()
@@ -453,10 +474,10 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
rate=1000, rate=1000,
serial_no=[serial_nos[0]],
do_not_save=1, do_not_save=1,
) )
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append( pos2.append(
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
) )
@@ -473,7 +494,7 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
) )
serial_nos = se.get("items")[0].serial_no + "wrong" serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] + "wrong"
pos = create_pos_invoice( pos = create_pos_invoice(
company="_Test Company", company="_Test Company",
@@ -486,14 +507,13 @@ class TestPOSInvoice(unittest.TestCase):
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
rate=1000, rate=1000,
qty=2, qty=2,
serial_nos=[serial_nos],
do_not_save=1, do_not_save=1,
) )
pos.get("items")[0].has_serial_no = 1 pos.get("items")[0].has_serial_no = 1
pos.get("items")[0].serial_no = serial_nos
pos.insert()
self.assertRaises(frappe.ValidationError, pos.submit) self.assertRaises(frappe.ValidationError, pos.insert)
def test_value_error_on_serial_no_validation(self): def test_value_error_on_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -504,7 +524,7 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
expense_account="Cost of Goods Sold - _TC", expense_account="Cost of Goods Sold - _TC",
) )
serial_nos = se.get("items")[0].serial_no serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
# make a pos invoice # make a pos invoice
pos = create_pos_invoice( pos = create_pos_invoice(
@@ -517,11 +537,11 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
rate=1000, rate=1000,
serial_no=[serial_nos[0]],
qty=1, qty=1,
do_not_save=1, do_not_save=1,
) )
pos.get("items")[0].has_serial_no = 1 pos.get("items")[0].has_serial_no = 1
pos.get("items")[0].serial_no = serial_nos.split("\n")[0]
pos.set("payments", []) pos.set("payments", [])
pos.append( pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
@@ -547,12 +567,12 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC", cost_center="Main - _TC",
item=se.get("items")[0].item_code, item=se.get("items")[0].item_code,
rate=1000, rate=1000,
serial_no=[serial_nos[0]],
qty=1, qty=1,
do_not_save=1, do_not_save=1,
) )
pos2.get("items")[0].has_serial_no = 1 pos2.get("items")[0].has_serial_no = 1
pos2.get("items")[0].serial_no = serial_nos.split("\n")[0]
# Value error should not be triggered on validation # Value error should not be triggered on validation
pos2.save() pos2.save()
@@ -748,16 +768,16 @@ class TestPOSInvoice(unittest.TestCase):
self.assertEqual(rounded_total, 400) self.assertEqual(rounded_total, 400)
def test_pos_batch_item_qty_validation(self): def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
BatchNegativeStockError,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_batch_item_with_batch, create_batch_item_with_batch,
) )
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01") create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01")
item = frappe.get_doc("Item", "_BATCH ITEM") item = frappe.get_doc("Item", "_BATCH ITEM")
batch = frappe.get_doc("Batch", "TestBatch 01")
batch.submit()
item.batch_no = "TestBatch 01"
item.save()
se = make_stock_entry( se = make_stock_entry(
target="_Test Warehouse - _TC", target="_Test Warehouse - _TC",
@@ -767,16 +787,28 @@ class TestPOSInvoice(unittest.TestCase):
batch_no="TestBatch 01", batch_no="TestBatch 01",
) )
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1) pos_inv1 = create_pos_invoice(
pos_inv1.items[0].batch_no = "TestBatch 01" item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
)
pos_inv1.save() pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1) pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
pos_inv2.items[0].batch_no = "TestBatch 01"
pos_inv2.save()
self.assertRaises(frappe.ValidationError, pos_inv2.submit) sn_doc = SerialBatchCreation(
{
"item_code": item.name,
"warehouse": pos_inv2.items[0].warehouse,
"voucher_type": "Delivery Note",
"qty": 2,
"avg_rate": 300,
"batches": frappe._dict({"TestBatch 01": 2}),
"type_of_transaction": "Outward",
"company": pos_inv2.company,
}
)
self.assertRaises(BatchNegativeStockError, sn_doc.make_serial_and_batch_bundle)
# teardown # teardown
pos_inv1.reload() pos_inv1.reload()
@@ -785,9 +817,6 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv2.reload() pos_inv2.reload()
pos_inv2.delete() pos_inv2.delete()
se.cancel() se.cancel()
batch.reload()
batch.cancel()
batch.delete()
def test_ignore_pricing_rule(self): def test_ignore_pricing_rule(self):
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
@@ -838,18 +867,18 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.savepoint("before_test_delivered_serial_no_case") frappe.db.savepoint("before_test_delivered_serial_no_case")
try: try:
se = make_serialized_item() se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=[serial_no])
delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0]
delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") self.assertEqual(serial_no, delivered_serial_no)
self.assertEquals(delivery_document_no, dn.name)
init_user_and_profile() init_user_and_profile()
pos_inv = create_pos_invoice( pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series", item_code="_Test Serialized Item With Series",
serial_no=serial_no, serial_no=[serial_no],
qty=1, qty=1,
rate=100, rate=100,
do_not_submit=True, do_not_submit=True,
@@ -861,42 +890,6 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.rollback(save_point="before_test_delivered_serial_no_case") frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
frappe.set_user("Administrator") frappe.set_user("Administrator")
def test_returned_serial_no_case(self):
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
frappe.db.savepoint("before_test_returned_serial_no_case")
try:
se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
qty=1,
rate=100,
)
pos_return = make_sales_return(pos_inv.name)
pos_return.flags.ignore_validate = True
pos_return.insert()
pos_return.submit()
pos_reserved_serial_nos = get_pos_reserved_serial_nos(
{"item_code": "_Test Serialized Item With Series", "warehouse": "_Test Warehouse - _TC"}
)
self.assertTrue(serial_no not in pos_reserved_serial_nos)
finally:
frappe.db.rollback(save_point="before_test_returned_serial_no_case")
frappe.set_user("Administrator")
def create_pos_invoice(**args): def create_pos_invoice(**args):
args = frappe._dict(args) args = frappe._dict(args)
@@ -926,6 +919,40 @@ def create_pos_invoice(**args):
pos_inv.set_missing_values() pos_inv.set_missing_values()
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
type_of_transaction = args.type_of_transaction or "Outward"
if pos_inv.is_return:
type_of_transaction = "Inward"
qty = args.get("qty") or 1
qty *= -1 if type_of_transaction == "Outward" else 1
batches = {}
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Delivery Note",
"serial_nos": args.serial_no,
"posting_date": pos_inv.posting_date,
"posting_time": pos_inv.posting_time,
"type_of_transaction": type_of_transaction,
"do_not_submit": True,
}
)
).name
if not bundle_id:
msg = f"Serial No {args.serial_no} not available for Item {args.item}"
frappe.throw(_(msg))
pos_inv.append( pos_inv.append(
"items", "items",
{ {
@@ -936,8 +963,7 @@ def create_pos_invoice(**args):
"income_account": args.income_account or "Sales - _TC", "income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no, "serial_and_batch_bundle": bundle_id,
"batch_no": args.batch_no,
}, },
) )

View File

@@ -79,6 +79,7 @@
"warehouse", "warehouse",
"target_warehouse", "target_warehouse",
"quality_inspection", "quality_inspection",
"serial_and_batch_bundle",
"batch_no", "batch_no",
"col_break5", "col_break5",
"allow_zero_valuation_rate", "allow_zero_valuation_rate",
@@ -628,10 +629,11 @@
{ {
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "hidden": 1,
"label": "Batch No", "label": "Batch No",
"options": "Batch", "options": "Batch",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "col_break5", "fieldname": "col_break5",
@@ -648,10 +650,12 @@
{ {
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Serial No", "label": "Serial No",
"oldfieldname": "serial_no", "oldfieldname": "serial_no",
"oldfieldtype": "Small Text" "oldfieldtype": "Small Text",
"read_only": 1
}, },
{ {
"fieldname": "item_tax_rate", "fieldname": "item_tax_rate",
@@ -817,11 +821,19 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Has Item Scanned", "label": "Has Item Scanned",
"read_only": 1 "read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-02 12:52:39.125295", "modified": "2023-03-12 13:36:40.160468",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Item", "name": "POS Invoice Item",

View File

@@ -184,6 +184,8 @@ class POSInvoiceMergeLog(Document):
item.base_amount = item.base_net_amount item.base_amount = item.base_net_amount
item.price_list_rate = 0 item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
if item.serial_and_batch_bundle:
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
items.append(si_item) items.append(si_item)
for tax in doc.get("taxes"): for tax in doc.get("taxes"):
@@ -385,7 +387,7 @@ def split_invoices(invoices):
] ]
for pos_invoice in pos_return_docs: for pos_invoice in pos_return_docs:
for item in pos_invoice.items: for item in pos_invoice.items:
if not item.serial_no: if not item.serial_no and not item.serial_and_batch_bundle:
continue continue
return_against_is_added = any( return_against_is_added = any(

View File

@@ -13,6 +13,9 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices, consolidate_pos_invoices,
) )
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_serial_nos_from_bundle,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -410,13 +413,13 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
try: try:
se = make_serialized_item() se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
init_user_and_profile() init_user_and_profile()
pos_inv = create_pos_invoice( pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series", item_code="_Test Serialized Item With Series",
serial_no=serial_no, serial_no=[serial_no],
qty=1, qty=1,
rate=100, rate=100,
do_not_submit=1, do_not_submit=1,
@@ -430,7 +433,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv2 = create_pos_invoice( pos_inv2 = create_pos_invoice(
item_code="_Test Serialized Item With Series", item_code="_Test Serialized Item With Series",
serial_no=serial_no, serial_no=[serial_no],
qty=1, qty=1,
rate=100, rate=100,
do_not_submit=1, do_not_submit=1,

View File

@@ -237,10 +237,6 @@ def apply_pricing_rule(args, doc=None):
item_list = args.get("items") item_list = args.get("items")
args.pop("items") args.pop("items")
set_serial_nos_based_on_fifo = frappe.db.get_single_value(
"Stock Settings", "automatically_set_serial_nos_based_on_fifo"
)
item_code_list = tuple(item.get("item_code") for item in item_list) item_code_list = tuple(item.get("item_code") for item in item_list)
query_items = frappe.get_all( query_items = frappe.get_all(
"Item", "Item",
@@ -258,28 +254,9 @@ def apply_pricing_rule(args, doc=None):
data = get_pricing_rule_for_item(args_copy, doc=doc) data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data) out.append(data)
if (
serialized_items.get(item.get("item_code"))
and not item.get("serial_no")
and set_serial_nos_based_on_fifo
and not args.get("is_return")
):
out[0].update(get_serial_no_for_item(args_copy))
return out return out
def get_serial_no_for_item(args):
from erpnext.stock.get_item_details import get_serial_no
item_details = frappe._dict(
{"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no}
)
if args.get("parenttype") in ("Sales Invoice", "Delivery Note") and flt(args.stock_qty) > 0:
item_details.serial_no = get_serial_no(args)
return item_details
def update_pricing_rule_uom(pricing_rule, args): def update_pricing_rule_uom(pricing_rule, args):
child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get( child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
pricing_rule.apply_on pricing_rule.apply_on

View File

@@ -102,9 +102,6 @@ class PurchaseInvoice(BuyingController):
# validate service stop date to lie in between start and end date # validate service stop date to lie in between start and end date
validate_service_stop_date(self) validate_service_stop_date(self)
if self._action == "submit" and self.update_stock:
self.make_batches("warehouse")
self.validate_release_date() self.validate_release_date()
self.check_conversion_rate() self.check_conversion_rate()
self.validate_credit_to_acc() self.validate_credit_to_acc()
@@ -513,10 +510,6 @@ class PurchaseInvoice(BuyingController):
if self.is_old_subcontracting_flow: if self.is_old_subcontracting_flow:
self.set_consumed_qty_in_subcontract_order() self.set_consumed_qty_in_subcontract_order()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
# this sequence because outstanding may get -negative # this sequence because outstanding may get -negative
self.make_gl_entries() self.make_gl_entries()
@@ -1448,6 +1441,7 @@ class PurchaseInvoice(BuyingController):
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Payment Ledger Entry", "Payment Ledger Entry",
"Tax Withheld Vouchers", "Tax Withheld Vouchers",
"Serial and Batch Bundle",
) )
self.update_advance_tax_references(cancel=1) self.update_advance_tax_references(cancel=1)

View File

@@ -26,6 +26,11 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_taxes, get_taxes,
make_purchase_receipt, make_purchase_receipt,
) )
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
from erpnext.stock.tests.test_utils import StockTestMixin from erpnext.stock.tests.test_utils import StockTestMixin
@@ -888,14 +893,20 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
rejected_warehouse="_Test Rejected Warehouse - _TC", rejected_warehouse="_Test Rejected Warehouse - _TC",
allow_zero_valuation_rate=1, allow_zero_valuation_rate=1,
) )
pi.load_from_db()
serial_no = get_serial_nos_from_bundle(pi.get("items")[0].serial_and_batch_bundle)[0]
rejected_serial_no = get_serial_nos_from_bundle(
pi.get("items")[0].rejected_serial_and_batch_bundle
)[0]
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), frappe.db.get_value("Serial No", serial_no, "warehouse"),
pi.get("items")[0].warehouse, pi.get("items")[0].warehouse,
) )
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, "warehouse"), frappe.db.get_value("Serial No", rejected_serial_no, "warehouse"),
pi.get("items")[0].rejected_warehouse, pi.get("items")[0].rejected_warehouse,
) )
@@ -1652,7 +1663,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
) )
pi.load_from_db() pi.load_from_db()
batch_no = pi.items[0].batch_no batch_no = get_batch_from_bundle(pi.items[0].serial_and_batch_bundle)
self.assertTrue(batch_no) self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
@@ -1734,6 +1745,32 @@ def make_purchase_invoice(**args):
pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
pi.cost_center = args.parent_cost_center pi.cost_center = args.parent_cost_center
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
batches = {}
qty = args.qty or 5
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
serial_nos = args.get("serial_no") or []
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Purchase Invoice",
"serial_nos": serial_nos,
"type_of_transaction": "Inward",
"posting_date": args.posting_date or today(),
"posting_time": args.posting_time,
}
)
).name
pi.append( pi.append(
"items", "items",
{ {
@@ -1748,12 +1785,11 @@ def make_purchase_invoice(**args):
"discount_account": args.discount_account or None, "discount_account": args.discount_account or None,
"discount_amount": args.discount_amount or 0, "discount_amount": args.discount_amount or 0,
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_no": args.serial_no, "serial_and_batch_bundle": bundle_id,
"stock_uom": args.uom or "_Test UOM", "stock_uom": args.uom or "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or "", "asset_location": args.location or "",
"allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0,
}, },
@@ -1797,6 +1833,31 @@ def make_purchase_invoice_against_cost_center(**args):
if args.supplier_warehouse: if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
batches = {}
qty = args.qty or 5
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
serial_nos = args.get("serial_no") or []
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Purchase Receipt",
"serial_nos": serial_nos,
"posting_date": args.posting_date or today(),
"posting_time": args.posting_time,
}
)
).name
pi.append( pi.append(
"items", "items",
{ {
@@ -1807,12 +1868,11 @@ def make_purchase_invoice_against_cost_center(**args):
"rejected_qty": args.rejected_qty or 0, "rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50, "rate": args.rate or 50,
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_no": args.serial_no, "serial_and_batch_bundle": bundle_id,
"stock_uom": "_Test UOM", "stock_uom": "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
}, },
) )
if not args.do_not_save: if not args.do_not_save:

View File

@@ -64,9 +64,11 @@
"warehouse", "warehouse",
"from_warehouse", "from_warehouse",
"quality_inspection", "quality_inspection",
"serial_and_batch_bundle",
"serial_no", "serial_no",
"col_br_wh", "col_br_wh",
"rejected_warehouse", "rejected_warehouse",
"rejected_serial_and_batch_bundle",
"batch_no", "batch_no",
"rejected_serial_no", "rejected_serial_no",
"manufacture_details", "manufacture_details",
@@ -436,9 +438,10 @@
"depends_on": "eval:!doc.is_fixed_asset", "depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1,
"label": "Batch No", "label": "Batch No",
"no_copy": 1, "options": "Batch",
"options": "Batch" "read_only": 1
}, },
{ {
"fieldname": "col_br_wh", "fieldname": "col_br_wh",
@@ -448,8 +451,9 @@
"depends_on": "eval:!doc.is_fixed_asset", "depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"hidden": 1,
"label": "Serial No", "label": "Serial No",
"no_copy": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset", "depends_on": "eval:!doc.is_fixed_asset",
@@ -457,7 +461,8 @@
"fieldtype": "Text", "fieldtype": "Text",
"label": "Rejected Serial No", "label": "Rejected Serial No",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "accounting", "fieldname": "accounting",
@@ -875,12 +880,30 @@
"fieldname": "apply_tds", "fieldname": "apply_tds",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Apply TDS" "label": "Apply TDS"
},
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-29 13:01:20.438217", "modified": "2023-04-01 20:08:54.545160",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -36,13 +36,8 @@ from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.setup.doctype.company.company import update_company_current_month_sales from erpnext.setup.doctype.company.company import update_company_current_month_sales
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import ( from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
get_delivery_note_serial_no,
get_serial_nos,
update_serial_nos_after_submit,
)
form_grid_templates = {"items": "templates/form_grid/item_grid.html"} form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -129,9 +124,6 @@ class SalesInvoice(SellingController):
if not self.is_opening: if not self.is_opening:
self.is_opening = "No" self.is_opening = "No"
if self._action != "submit" and self.update_stock and not self.is_return:
set_batch_nos(self, "warehouse", True)
if self.redeem_loyalty_points: if self.redeem_loyalty_points:
lp = frappe.get_doc("Loyalty Program", self.loyalty_program) lp = frappe.get_doc("Loyalty Program", self.loyalty_program)
self.loyalty_redemption_account = ( self.loyalty_redemption_account = (
@@ -262,8 +254,6 @@ class SalesInvoice(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO # because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
if self.is_return and self.update_stock:
update_serial_nos_after_submit(self, "items")
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
@@ -276,8 +266,6 @@ class SalesInvoice(SellingController):
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit() self.check_credit_limit()
self.update_serial_no()
if not cint(self.is_pos) == 1 and not self.is_return: if not cint(self.is_pos) == 1 and not self.is_return:
self.update_against_document_in_jv() self.update_against_document_in_jv()
@@ -361,7 +349,6 @@ class SalesInvoice(SellingController):
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note") self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.update_serial_no(in_cancel=True)
# Updating stock ledger should always be called after updating prevdoc status, # Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO # because updating reserved qty in bin depends upon updated delivered qty in SO
@@ -400,6 +387,7 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger", "Repost Payment Ledger",
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Payment Ledger Entry", "Payment Ledger Entry",
"Serial and Batch Bundle",
) )
def update_status_updater_args(self): def update_status_updater_args(self):
@@ -1518,20 +1506,6 @@ class SalesInvoice(SellingController):
self.set("write_off_amount", reference_doc.get("write_off_amount")) self.set("write_off_amount", reference_doc.get("write_off_amount"))
self.due_date = None self.due_date = None
def update_serial_no(self, in_cancel=False):
"""update Sales Invoice refrence in Serial No"""
invoice = None if (in_cancel or self.is_return) else self.name
if in_cancel and self.is_return:
invoice = self.return_against
for item in self.items:
if not item.serial_no:
continue
for serial_no in get_serial_nos(item.serial_no):
if serial_no and frappe.db.get_value("Serial No", serial_no, "item_code") == item.item_code:
frappe.db.set_value("Serial No", serial_no, "sales_invoice", invoice)
def validate_serial_numbers(self): def validate_serial_numbers(self):
""" """
validate serial number agains Delivery Note and Sales Invoice validate serial number agains Delivery Note and Sales Invoice

View File

@@ -30,6 +30,11 @@ from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from erpnext.stock.doctype.stock_entry.test_stock_entry import ( from erpnext.stock.doctype.stock_entry.test_stock_entry import (
get_qty_after_transaction, get_qty_after_transaction,
@@ -1348,55 +1353,47 @@ class TestSalesInvoice(unittest.TestCase):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item() se = make_serialized_item()
serial_nos = get_serial_nos(se.get("items")[0].serial_no) se.load_from_db()
serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
si = frappe.copy_doc(test_records[0]) si = frappe.copy_doc(test_records[0])
si.update_stock = 1 si.update_stock = 1
si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].item_code = "_Test Serialized Item With Series"
si.get("items")[0].qty = 1 si.get("items")[0].qty = 1
si.get("items")[0].serial_no = serial_nos[0] si.get("items")[0].warehouse = se.get("items")[0].t_warehouse
si.get("items")[0].serial_and_batch_bundle = make_serial_batch_bundle(
frappe._dict(
{
"item_code": si.get("items")[0].item_code,
"warehouse": si.get("items")[0].warehouse,
"company": si.company,
"qty": 1,
"voucher_type": "Stock Entry",
"serial_nos": [serial_nos[0]],
"posting_date": si.posting_date,
"posting_time": si.posting_time,
"type_of_transaction": "Outward",
"do_not_submit": True,
}
)
).name
si.insert() si.insert()
si.submit() si.submit()
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name
)
return si return si
def test_serialized_cancel(self): def test_serialized_cancel(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
si = self.test_serialized() si = self.test_serialized()
si.cancel() si.cancel()
serial_nos = get_serial_nos(si.get("items")[0].serial_no) serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
) )
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"))
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"))
def test_serialize_status(self):
serial_no = frappe.get_doc(
{
"doctype": "Serial No",
"item_code": "_Test Serialized Item With Series",
"serial_no": make_autoname("SR", "Serial No"),
}
)
serial_no.save()
si = frappe.copy_doc(test_records[0])
si.update_stock = 1
si.get("items")[0].item_code = "_Test Serialized Item With Series"
si.get("items")[0].qty = 1
si.get("items")[0].serial_no = serial_no.name
si.insert()
self.assertRaises(SerialNoWarehouseError, si.submit)
def test_serial_numbers_against_delivery_note(self): def test_serial_numbers_against_delivery_note(self):
""" """
@@ -1404,20 +1401,22 @@ class TestSalesInvoice(unittest.TestCase):
serial numbers are same serial numbers are same
""" """
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item() se = make_serialized_item()
serial_nos = get_serial_nos(se.get("items")[0].serial_no) se.load_from_db()
serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=serial_nos[0]) dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=[serial_nos])
dn.submit() dn.submit()
dn.load_from_db()
serial_nos = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0]
self.assertTrue(get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0])
si = make_sales_invoice(dn.name) si = make_sales_invoice(dn.name)
si.save() si.save()
self.assertEqual(si.get("items")[0].serial_no, dn.get("items")[0].serial_no)
def test_return_sales_invoice(self): def test_return_sales_invoice(self):
make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100)
@@ -2573,7 +2572,7 @@ class TestSalesInvoice(unittest.TestCase):
"posting_date": si.posting_date, "posting_date": si.posting_date,
"posting_time": si.posting_time, "posting_time": si.posting_time,
"qty": -1 * flt(d.get("stock_qty")), "qty": -1 * flt(d.get("stock_qty")),
"serial_no": d.serial_no, "serial_and_batch_bundle": d.serial_and_batch_bundle,
"company": si.company, "company": si.company,
"voucher_type": "Sales Invoice", "voucher_type": "Sales Invoice",
"voucher_no": si.name, "voucher_no": si.name,
@@ -2982,7 +2981,7 @@ class TestSalesInvoice(unittest.TestCase):
# Sales Invoice with Payment Schedule # Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True) si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend( si_with_payment_schedule.set(
"payment_schedule", "payment_schedule",
[ [
{ {
@@ -3174,7 +3173,7 @@ class TestSalesInvoice(unittest.TestCase):
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
) )
si.reload() si.reload()
self.assertTrue(si.items[0].serial_no) self.assertTrue(get_serial_nos_from_bundle(si.items[0].serial_and_batch_bundle))
def test_sales_invoice_with_disabled_account(self): def test_sales_invoice_with_disabled_account(self):
try: try:
@@ -3283,11 +3282,11 @@ class TestSalesInvoice(unittest.TestCase):
pr = make_purchase_receipt(qty=1, item_code=item.name) pr = make_purchase_receipt(qty=1, item_code=item.name)
batch_no = pr.items[0].batch_no batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no) si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
si.load_from_db() si.load_from_db()
batch_no = si.items[0].batch_no batch_no = get_batch_from_bundle(si.items[0].serial_and_batch_bundle)
self.assertTrue(batch_no) self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
@@ -3386,6 +3385,33 @@ def create_sales_invoice(**args):
si.naming_series = args.naming_series or "T-SINV-" si.naming_series = args.naming_series or "T-SINV-"
si.cost_center = args.parent_cost_center si.cost_center = args.parent_cost_center
bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {}
qty = args.qty or 1
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
serial_nos = args.get("serial_no") or []
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Sales Invoice",
"serial_nos": serial_nos,
"type_of_transaction": "Outward" if not args.is_return else "Inward",
"posting_date": si.posting_date or today(),
"posting_time": si.posting_time,
"do_not_submit": True,
}
)
).name
si.append( si.append(
"items", "items",
{ {
@@ -3405,10 +3431,9 @@ def create_sales_invoice(**args):
"discount_amount": args.discount_amount or 0, "discount_amount": args.discount_amount or 0,
"asset": args.asset or None, "asset": args.asset or None,
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": args.get("conversion_factor", 1), "conversion_factor": args.get("conversion_factor", 1),
"incoming_rate": args.incoming_rate or 0, "incoming_rate": args.incoming_rate or 0,
"batch_no": args.batch_no or None, "serial_and_batch_bundle": bundle_id,
}, },
) )
@@ -3418,6 +3443,8 @@ def create_sales_invoice(**args):
si.submit() si.submit()
else: else:
si.payment_schedule = [] si.payment_schedule = []
si.load_from_db()
else: else:
si.payment_schedule = [] si.payment_schedule = []
@@ -3452,7 +3479,6 @@ def create_sales_invoice_against_cost_center(**args):
"income_account": "Sales - _TC", "income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC", "expense_account": "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
}, },
) )

View File

@@ -81,6 +81,7 @@
"warehouse", "warehouse",
"target_warehouse", "target_warehouse",
"quality_inspection", "quality_inspection",
"serial_and_batch_bundle",
"batch_no", "batch_no",
"incoming_rate", "incoming_rate",
"col_break5", "col_break5",
@@ -600,10 +601,10 @@
{ {
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "hidden": 1,
"label": "Batch No", "label": "Batch No",
"options": "Batch", "options": "Batch",
"print_hide": 1 "read_only": 1
}, },
{ {
"fieldname": "col_break5", "fieldname": "col_break5",
@@ -620,10 +621,11 @@
{ {
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"in_list_view": 1, "hidden": 1,
"label": "Serial No", "label": "Serial No",
"oldfieldname": "serial_no", "oldfieldname": "serial_no",
"oldfieldtype": "Small Text" "oldfieldtype": "Small Text",
"read_only": 1
}, },
{ {
"fieldname": "item_group", "fieldname": "item_group",
@@ -885,12 +887,20 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Has Item Scanned", "label": "Has Item Scanned",
"read_only": 1 "read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-12-28 16:17:33.484531", "modified": "2023-03-12 13:42:24.303113",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr from frappe.utils import cstr
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
get_columns, get_columns,
@@ -20,11 +20,6 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None): def execute(filters=None):
if cint(frappe.db.get_single_value("Accounts Settings", "use_custom_cash_flow")):
from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom
return execute_custom(filters=filters)
period_list = get_period_list( period_list = get_period_list(
filters.from_fiscal_year, filters.from_fiscal_year,
filters.to_fiscal_year, filters.to_fiscal_year,

View File

@@ -1,567 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import add_to_date, flt, get_date_str
from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_net_profit_loss,
)
def get_mapper_for(mappers, position):
mapper_list = list(filter(lambda x: x["position"] == position, mappers))
return mapper_list[0] if mapper_list else []
def get_mappers_from_db():
return frappe.get_all(
"Cash Flow Mapper",
fields=[
"section_name",
"section_header",
"section_leader",
"section_subtotal",
"section_footer",
"name",
"position",
],
order_by="position",
)
def get_accounts_in_mappers(mapping_names):
cfm = frappe.qb.DocType("Cash Flow Mapping")
cfma = frappe.qb.DocType("Cash Flow Mapping Accounts")
result = (
frappe.qb.select(
cfma.name,
cfm.label,
cfm.is_working_capital,
cfm.is_income_tax_liability,
cfm.is_income_tax_expense,
cfm.is_finance_cost,
cfm.is_finance_cost_adjustment,
cfma.account,
)
.from_(cfm)
.join(cfma)
.on(cfm.name == cfma.parent)
.where(cfma.parent.isin(mapping_names))
).run()
return result
def setup_mappers(mappers):
cash_flow_accounts = []
for mapping in mappers:
mapping["account_types"] = []
mapping["tax_liabilities"] = []
mapping["tax_expenses"] = []
mapping["finance_costs"] = []
mapping["finance_costs_adjustments"] = []
doc = frappe.get_doc("Cash Flow Mapper", mapping["name"])
mapping_names = [item.name for item in doc.accounts]
if not mapping_names:
continue
accounts = get_accounts_in_mappers(mapping_names)
account_types = [
dict(
name=account[0],
account_name=account[7],
label=account[1],
is_working_capital=account[2],
is_income_tax_liability=account[3],
is_income_tax_expense=account[4],
)
for account in accounts
if not account[3]
]
finance_costs_adjustments = [
dict(
name=account[0],
account_name=account[7],
label=account[1],
is_finance_cost=account[5],
is_finance_cost_adjustment=account[6],
)
for account in accounts
if account[6]
]
tax_liabilities = [
dict(
name=account[0],
account_name=account[7],
label=account[1],
is_income_tax_liability=account[3],
is_income_tax_expense=account[4],
)
for account in accounts
if account[3]
]
tax_expenses = [
dict(
name=account[0],
account_name=account[7],
label=account[1],
is_income_tax_liability=account[3],
is_income_tax_expense=account[4],
)
for account in accounts
if account[4]
]
finance_costs = [
dict(name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5])
for account in accounts
if account[5]
]
account_types_labels = sorted(
set(
(d["label"], d["is_working_capital"], d["is_income_tax_liability"], d["is_income_tax_expense"])
for d in account_types
),
key=lambda x: x[1],
)
fc_adjustment_labels = sorted(
set(
[
(d["label"], d["is_finance_cost"], d["is_finance_cost_adjustment"])
for d in finance_costs_adjustments
if d["is_finance_cost_adjustment"]
]
),
key=lambda x: x[2],
)
unique_liability_labels = sorted(
set(
[
(d["label"], d["is_income_tax_liability"], d["is_income_tax_expense"])
for d in tax_liabilities
]
),
key=lambda x: x[0],
)
unique_expense_labels = sorted(
set(
[(d["label"], d["is_income_tax_liability"], d["is_income_tax_expense"]) for d in tax_expenses]
),
key=lambda x: x[0],
)
unique_finance_costs_labels = sorted(
set([(d["label"], d["is_finance_cost"]) for d in finance_costs]), key=lambda x: x[0]
)
for label in account_types_labels:
names = [d["account_name"] for d in account_types if d["label"] == label[0]]
m = dict(label=label[0], names=names, is_working_capital=label[1])
mapping["account_types"].append(m)
for label in fc_adjustment_labels:
names = [d["account_name"] for d in finance_costs_adjustments if d["label"] == label[0]]
m = dict(label=label[0], names=names)
mapping["finance_costs_adjustments"].append(m)
for label in unique_liability_labels:
names = [d["account_name"] for d in tax_liabilities if d["label"] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping["tax_liabilities"].append(m)
for label in unique_expense_labels:
names = [d["account_name"] for d in tax_expenses if d["label"] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping["tax_expenses"].append(m)
for label in unique_finance_costs_labels:
names = [d["account_name"] for d in finance_costs if d["label"] == label[0]]
m = dict(label=label[0], names=names, is_finance_cost=label[1])
mapping["finance_costs"].append(m)
cash_flow_accounts.append(mapping)
return cash_flow_accounts
def add_data_for_operating_activities(
filters, company_currency, profit_data, period_list, light_mappers, mapper, data
):
has_added_working_capital_header = False
section_data = []
data.append(
{
"account_name": mapper["section_header"],
"parent_account": None,
"indent": 0.0,
"account": mapper["section_header"],
}
)
if profit_data:
profit_data.update(
{"indent": 1, "parent_account": get_mapper_for(light_mappers, position=1)["section_header"]}
)
data.append(profit_data)
section_data.append(profit_data)
data.append(
{
"account_name": mapper["section_leader"],
"parent_account": None,
"indent": 1.0,
"account": mapper["section_leader"],
}
)
for account in mapper["account_types"]:
if account["is_working_capital"] and not has_added_working_capital_header:
data.append(
{
"account_name": "Movement in working capital",
"parent_account": None,
"indent": 1.0,
"account": "",
}
)
has_added_working_capital_header = True
account_data = _get_account_type_based_data(
filters, account["names"], period_list, filters.accumulated_values
)
if not account["is_working_capital"]:
for key in account_data:
if key != "total":
account_data[key] *= -1
if account_data["total"] != 0:
account_data.update(
{
"account_name": account["label"],
"account": account["names"],
"indent": 1.0,
"parent_account": mapper["section_header"],
"currency": company_currency,
}
)
data.append(account_data)
section_data.append(account_data)
_add_total_row_account(
data, section_data, mapper["section_subtotal"], period_list, company_currency, indent=1
)
# calculate adjustment for tax paid and add to data
if not mapper["tax_liabilities"]:
mapper["tax_liabilities"] = [
dict(label="Income tax paid", names=[""], tax_liability=1, tax_expense=0)
]
for account in mapper["tax_liabilities"]:
tax_paid = calculate_adjustment(
filters,
mapper["tax_liabilities"],
mapper["tax_expenses"],
filters.accumulated_values,
period_list,
)
if tax_paid:
tax_paid.update(
{
"parent_account": mapper["section_header"],
"currency": company_currency,
"account_name": account["label"],
"indent": 1.0,
}
)
data.append(tax_paid)
section_data.append(tax_paid)
if not mapper["finance_costs_adjustments"]:
mapper["finance_costs_adjustments"] = [dict(label="Interest Paid", names=[""])]
for account in mapper["finance_costs_adjustments"]:
interest_paid = calculate_adjustment(
filters,
mapper["finance_costs_adjustments"],
mapper["finance_costs"],
filters.accumulated_values,
period_list,
)
if interest_paid:
interest_paid.update(
{
"parent_account": mapper["section_header"],
"currency": company_currency,
"account_name": account["label"],
"indent": 1.0,
}
)
data.append(interest_paid)
section_data.append(interest_paid)
_add_total_row_account(
data, section_data, mapper["section_footer"], period_list, company_currency
)
def calculate_adjustment(
filters, non_expense_mapper, expense_mapper, use_accumulated_values, period_list
):
liability_accounts = [d["names"] for d in non_expense_mapper]
expense_accounts = [d["names"] for d in expense_mapper]
non_expense_closing = _get_account_type_based_data(filters, liability_accounts, period_list, 0)
non_expense_opening = _get_account_type_based_data(
filters, liability_accounts, period_list, use_accumulated_values, opening_balances=1
)
expense_data = _get_account_type_based_data(
filters, expense_accounts, period_list, use_accumulated_values
)
data = _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data)
return data
def _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data):
account_data = {}
for month in non_expense_opening.keys():
if non_expense_opening[month] and non_expense_closing[month]:
account_data[month] = (
non_expense_opening[month] - expense_data[month] + non_expense_closing[month]
)
elif expense_data[month]:
account_data[month] = expense_data[month]
return account_data
def add_data_for_other_activities(
filters, company_currency, profit_data, period_list, light_mappers, mapper_list, data
):
for mapper in mapper_list:
section_data = []
data.append(
{
"account_name": mapper["section_header"],
"parent_account": None,
"indent": 0.0,
"account": mapper["section_header"],
}
)
for account in mapper["account_types"]:
account_data = _get_account_type_based_data(
filters, account["names"], period_list, filters.accumulated_values
)
if account_data["total"] != 0:
account_data.update(
{
"account_name": account["label"],
"account": account["names"],
"indent": 1,
"parent_account": mapper["section_header"],
"currency": company_currency,
}
)
data.append(account_data)
section_data.append(account_data)
_add_total_row_account(
data, section_data, mapper["section_footer"], period_list, company_currency
)
def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper):
data = []
operating_activities_mapper = get_mapper_for(light_mappers, position=1)
other_mappers = [
get_mapper_for(light_mappers, position=2),
get_mapper_for(light_mappers, position=3),
]
if operating_activities_mapper:
add_data_for_operating_activities(
filters,
company_currency,
profit_data,
period_list,
light_mappers,
operating_activities_mapper,
data,
)
if all(other_mappers):
add_data_for_other_activities(
filters, company_currency, profit_data, period_list, light_mappers, other_mappers, data
)
return data
def execute(filters=None):
if not filters.periodicity:
filters.periodicity = "Monthly"
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
filters.period_start_date,
filters.period_end_date,
filters.filter_based_on,
filters.periodicity,
company=filters.company,
)
mappers = get_mappers_from_db()
cash_flow_accounts = setup_mappers(mappers)
# compute net profit / loss
income = get_data(
filters.company,
"Income",
"Credit",
period_list,
filters=filters,
accumulated_values=filters.accumulated_values,
ignore_closing_entries=True,
ignore_accumulated_values_for_fy=True,
)
expense = get_data(
filters.company,
"Expense",
"Debit",
period_list,
filters=filters,
accumulated_values=filters.accumulated_values,
ignore_closing_entries=True,
ignore_accumulated_values_for_fy=True,
)
net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company)
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
data = compute_data(
filters, company_currency, net_profit_loss, period_list, mappers, cash_flow_accounts
)
_add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency)
columns = get_columns(
filters.periodicity, period_list, filters.accumulated_values, filters.company
)
return columns, data
def _get_account_type_based_data(
filters, account_names, period_list, accumulated_values, opening_balances=0
):
if not account_names or not account_names[0] or not type(account_names[0]) == str:
# only proceed if account_names is a list of account names
return {}
from erpnext.accounts.report.cash_flow.cash_flow import get_start_date
company = filters.company
data = {}
total = 0
GLEntry = frappe.qb.DocType("GL Entry")
Account = frappe.qb.DocType("Account")
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
account_subquery = (
frappe.qb.from_(Account)
.where((Account.name.isin(account_names)) | (Account.parent_account.isin(account_names)))
.select(Account.name)
.as_("account_subquery")
)
if opening_balances:
date_info = dict(date=start_date)
months_map = {"Monthly": -1, "Quarterly": -3, "Half-Yearly": -6}
years_map = {"Yearly": -1}
if months_map.get(filters.periodicity):
date_info.update(months=months_map[filters.periodicity])
else:
date_info.update(years=years_map[filters.periodicity])
if accumulated_values:
start, end = add_to_date(start_date, years=-1), add_to_date(period["to_date"], years=-1)
else:
start, end = add_to_date(**date_info), add_to_date(**date_info)
start, end = get_date_str(start), get_date_str(end)
else:
start, end = start_date if accumulated_values else period["from_date"], period["to_date"]
start, end = get_date_str(start), get_date_str(end)
result = (
frappe.qb.from_(GLEntry)
.select(Sum(GLEntry.credit) - Sum(GLEntry.debit))
.where(
(GLEntry.company == company)
& (GLEntry.posting_date >= start)
& (GLEntry.posting_date <= end)
& (GLEntry.voucher_type != "Period Closing Voucher")
& (GLEntry.account.isin(account_subquery))
)
).run()
if result and result[0]:
gl_sum = result[0][0]
else:
gl_sum = 0
total += flt(gl_sum)
data.setdefault(period["key"], flt(gl_sum))
data["total"] = total
return data
def _add_total_row_account(out, data, label, period_list, currency, indent=0.0):
total_row = {
"indent": indent,
"account_name": "'" + _("{0}").format(label) + "'",
"account": "'" + _("{0}").format(label) + "'",
"currency": currency,
}
for row in data:
if row.get("parent_account"):
for period in period_list:
total_row.setdefault(period.key, 0.0)
total_row[period.key] += row.get(period.key, 0.0)
total_row.setdefault("total", 0.0)
total_row["total"] += row["total"]
out.append(total_row)
out.append({})

View File

@@ -6,7 +6,7 @@ from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, flt, getdate from frappe.utils import flt, getdate
import erpnext import erpnext
from erpnext.accounts.report.balance_sheet.balance_sheet import ( from erpnext.accounts.report.balance_sheet.balance_sheet import (
@@ -58,11 +58,6 @@ def execute(filters=None):
fiscal_year, companies, columns, filters fiscal_year, companies, columns, filters
) )
else: else:
if cint(frappe.db.get_single_value("Accounts Settings", "use_custom_cash_flow")):
from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom
return execute_custom(filters=filters)
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters) data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
return columns, data, message, chart, report_summary return columns, data, message, chart, report_summary

View File

@@ -703,6 +703,9 @@ class GrossProfitGenerator(object):
} }
) )
if row.serial_and_batch_bundle:
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
average_buying_rate = get_incoming_rate(args) average_buying_rate = get_incoming_rate(args)
self.average_buying_rate[item_code] = flt(average_buying_rate) self.average_buying_rate[item_code] = flt(average_buying_rate)
@@ -805,7 +808,7 @@ class GrossProfitGenerator(object):
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
`tabSales Invoice Item`.cost_center `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
{sales_person_cols} {sales_person_cols}
{payment_term_cols} {payment_term_cols}
from from

View File

@@ -6,6 +6,7 @@ frappe.provide("erpnext.assets");
erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController {
setup() { setup() {
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
} }

View File

@@ -334,7 +334,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-09-12 15:09:40.771332", "modified": "2022-10-12 15:09:40.771332",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Capitalization", "name": "Asset Capitalization",

View File

@@ -65,6 +65,10 @@ class AssetCapitalization(StockController):
self.calculate_totals() self.calculate_totals()
self.set_title() self.set_title()
def on_update(self):
if self.stock_items:
self.set_serial_and_batch_bundle(table_name="stock_items")
def before_submit(self): def before_submit(self):
self.validate_source_mandatory() self.validate_source_mandatory()
@@ -74,7 +78,12 @@ class AssetCapitalization(StockController):
self.update_target_asset() self.update_target_asset()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Serial and Batch Bundle",
)
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries() self.make_gl_entries()
self.update_target_asset() self.update_target_asset()
@@ -316,9 +325,7 @@ class AssetCapitalization(StockController):
for d in self.stock_items: for d in self.stock_items:
sle = self.get_sl_entries( sle = self.get_sl_entries(
d, d,
{ {"actual_qty": -flt(d.stock_qty), "serial_and_batch_bundle": d.serial_and_batch_bundle},
"actual_qty": -flt(d.stock_qty),
},
) )
sl_entries.append(sle) sl_entries.append(sle)
@@ -328,8 +335,6 @@ class AssetCapitalization(StockController):
{ {
"item_code": self.target_item_code, "item_code": self.target_item_code,
"warehouse": self.target_warehouse, "warehouse": self.target_warehouse,
"batch_no": self.target_batch_no,
"serial_no": self.target_serial_no,
"actual_qty": flt(self.target_qty), "actual_qty": flt(self.target_qty),
"incoming_rate": flt(self.target_incoming_rate), "incoming_rate": flt(self.target_incoming_rate),
}, },

View File

@@ -16,6 +16,11 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
) )
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
class TestAssetCapitalization(unittest.TestCase): class TestAssetCapitalization(unittest.TestCase):
@@ -371,14 +376,32 @@ def create_asset_capitalization(**args):
asset_capitalization.set_posting_time = 1 asset_capitalization.set_posting_time = 1
if flt(args.stock_rate): if flt(args.stock_rate):
bundle = None
if args.stock_batch_no or args.stock_serial_no:
bundle = make_serial_batch_bundle(
frappe._dict(
{
"item_code": args.stock_item,
"warehouse": source_warehouse,
"company": frappe.get_cached_value("Warehouse", source_warehouse, "company"),
"qty": (flt(args.stock_qty) or 1) * -1,
"voucher_type": "Asset Capitalization",
"type_of_transaction": "Outward",
"serial_nos": args.stock_serial_no,
"posting_date": asset_capitalization.posting_date,
"posting_time": asset_capitalization.posting_time,
"do_not_submit": True,
}
)
).name
asset_capitalization.append( asset_capitalization.append(
"stock_items", "stock_items",
{ {
"item_code": args.stock_item or "Capitalization Source Stock Item", "item_code": args.stock_item or "Capitalization Source Stock Item",
"warehouse": source_warehouse, "warehouse": source_warehouse,
"stock_qty": flt(args.stock_qty) or 1, "stock_qty": flt(args.stock_qty) or 1,
"batch_no": args.stock_batch_no, "serial_and_batch_bundle": bundle,
"serial_no": args.stock_serial_no,
}, },
) )

View File

@@ -17,8 +17,9 @@
"valuation_rate", "valuation_rate",
"amount", "amount",
"batch_and_serial_no_section", "batch_and_serial_no_section",
"batch_no", "serial_and_batch_bundle",
"column_break_13", "column_break_13",
"batch_no",
"serial_no", "serial_no",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
@@ -41,7 +42,10 @@
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
"options": "Batch" "no_copy": 1,
"options": "Batch",
"print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
@@ -100,7 +104,10 @@
{ {
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Serial No" "hidden": 1,
"label": "Serial No",
"print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "item_code", "fieldname": "item_code",
@@ -139,12 +146,20 @@
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-09-08 15:56:20.230548", "modified": "2023-04-06 01:10:17.947952",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Capitalization Stock Item", "name": "Asset Capitalization Stock Item",
@@ -152,5 +167,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -147,6 +147,8 @@ class AssetRepair(AccountsController):
) )
for stock_item in self.get("stock_items"): for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
stock_entry.append( stock_entry.append(
"items", "items",
{ {
@@ -154,7 +156,7 @@ class AssetRepair(AccountsController):
"item_code": stock_item.item_code, "item_code": stock_item.item_code,
"qty": stock_item.consumed_quantity, "qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate, "basic_rate": stock_item.valuation_rate,
"serial_no": stock_item.serial_no, "serial_no": stock_item.serial_and_batch_bundle,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"project": self.project, "project": self.project,
}, },
@@ -165,6 +167,23 @@ class AssetRepair(AccountsController):
self.db_set("stock_entry", stock_entry.name) self.db_set("stock_entry", stock_entry.name)
def validate_serial_no(self, stock_item):
if not stock_item.serial_and_batch_bundle and frappe.get_cached_value(
"Item", stock_item.item_code, "has_serial_no"
):
msg = f"Serial No Bundle is mandatory for Item {stock_item.item_code}"
frappe.throw(msg, title=_("Missing Serial No Bundle"))
if stock_item.serial_and_batch_bundle:
values_to_update = {
"type_of_transaction": "Outward",
"voucher_type": "Stock Entry",
}
frappe.db.set_value(
"Serial and Batch Bundle", stock_item.serial_and_batch_bundle, values_to_update
)
def increase_stock_quantity(self): def increase_stock_quantity(self):
if self.stock_entry: if self.stock_entry:
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)

View File

@@ -4,7 +4,7 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate, nowtime, today
from erpnext.assets.doctype.asset.asset import ( from erpnext.assets.doctype.asset.asset import (
get_asset_account, get_asset_account,
@@ -19,6 +19,10 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
) )
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
class TestAssetRepair(unittest.TestCase): class TestAssetRepair(unittest.TestCase):
@@ -84,19 +88,19 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
def test_serialized_item_consumption(self): def test_serialized_item_consumption(self):
from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
stock_entry = make_serialized_item() stock_entry = make_serialized_item()
serial_nos = stock_entry.get("items")[0].serial_no bundle_id = stock_entry.get("items")[0].serial_and_batch_bundle
serial_no = serial_nos.split("\n")[0] serial_nos = get_serial_nos_from_bundle(bundle_id)
serial_no = serial_nos[0]
# should not raise any error # should not raise any error
create_asset_repair( create_asset_repair(
stock_consumption=1, stock_consumption=1,
item_code=stock_entry.get("items")[0].item_code, item_code=stock_entry.get("items")[0].item_code,
warehouse="_Test Warehouse - _TC", warehouse="_Test Warehouse - _TC",
serial_no=serial_no, serial_no=[serial_no],
submit=1, submit=1,
) )
@@ -108,7 +112,7 @@ class TestAssetRepair(unittest.TestCase):
) )
asset_repair.repair_status = "Completed" asset_repair.repair_status = "Completed"
self.assertRaises(SerialNoRequiredError, asset_repair.submit) self.assertRaises(frappe.ValidationError, asset_repair.submit)
def test_increase_in_asset_value_due_to_stock_consumption(self): def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation=1, submit=1) asset = create_asset(calculate_depreciation=1, submit=1)
@@ -290,13 +294,32 @@ def create_asset_repair(**args):
asset_repair.warehouse = args.warehouse or create_warehouse( asset_repair.warehouse = args.warehouse or create_warehouse(
"Test Warehouse", company=asset.company "Test Warehouse", company=asset.company
) )
bundle = None
if args.serial_no:
bundle = make_serial_batch_bundle(
frappe._dict(
{
"item_code": args.item_code,
"warehouse": asset_repair.warehouse,
"company": frappe.get_cached_value("Warehouse", asset_repair.warehouse, "company"),
"qty": (flt(args.stock_qty) or 1) * -1,
"voucher_type": "Asset Repair",
"type_of_transaction": "Asset Repair",
"serial_nos": args.serial_no,
"posting_date": today(),
"posting_time": nowtime(),
}
)
).name
asset_repair.append( asset_repair.append(
"stock_items", "stock_items",
{ {
"item_code": args.item_code or "_Test Stock Item", "item_code": args.item_code or "_Test Stock Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100, "valuation_rate": args.rate if args.get("rate") is not None else 100,
"consumed_quantity": args.qty or 1, "consumed_quantity": args.qty or 1,
"serial_no": args.serial_no, "serial_and_batch_bundle": bundle,
}, },
) )

View File

@@ -9,7 +9,8 @@
"valuation_rate", "valuation_rate",
"consumed_quantity", "consumed_quantity",
"total_value", "total_value",
"serial_no" "serial_no",
"serial_and_batch_bundle"
], ],
"fields": [ "fields": [
{ {
@@ -34,7 +35,9 @@
{ {
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Serial No" "hidden": 1,
"label": "Serial No",
"print_hide": 1
}, },
{ {
"fieldname": "item_code", "fieldname": "item_code",
@@ -42,12 +45,18 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Item", "label": "Item",
"options": "Item" "options": "Item"
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"options": "Serial and Batch Bundle"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-02-08 17:37:20.028290", "modified": "2023-04-06 02:24:20.375870",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair Consumed Item", "name": "Asset Repair Consumed Item",
@@ -55,5 +64,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -758,6 +758,7 @@ class AccountsController(TransactionBase):
} }
) )
update_gl_dict_with_regional_fields(self, gl_dict)
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
dimension_dict = frappe._dict() dimension_dict = frappe._dict()
@@ -2846,3 +2847,8 @@ def validate_regional(doc):
@erpnext.allow_regional @erpnext.allow_regional
def validate_einvoice_fields(doc): def validate_einvoice_fields(doc):
pass pass
@erpnext.allow_regional
def update_gl_dict_with_regional_fields(doc, gl_dict):
pass

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import ValidationError, _, msgprint from frappe import ValidationError, _, msgprint
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, getdate from frappe.utils import cint, flt, getdate
from frappe.utils.data import nowtime from frappe.utils.data import nowtime
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
@@ -38,6 +38,7 @@ class BuyingController(SubcontractingController):
self.set_supplier_address() self.set_supplier_address()
self.validate_asset_return() self.validate_asset_return()
self.validate_auto_repeat_subscription_dates() self.validate_auto_repeat_subscription_dates()
self.create_package_for_transfer()
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
self.validate_purchase_receipt_if_update_stock() self.validate_purchase_receipt_if_update_stock()
@@ -58,6 +59,7 @@ class BuyingController(SubcontractingController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate() self.update_valuation_rate()
self.set_serial_and_batch_bundle()
def onload(self): def onload(self):
super(BuyingController, self).onload() super(BuyingController, self).onload()
@@ -68,6 +70,36 @@ class BuyingController(SubcontractingController):
), ),
) )
def create_package_for_transfer(self) -> None:
"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
if self.is_internal_transfer() and (
self.doctype == "Purchase Receipt" or (self.doctype == "Purchase Invoice" and self.update_stock)
):
field = "delivery_note_item" if self.doctype == "Purchase Receipt" else "sales_invoice_item"
doctype = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
ids = [d.get(field) for d in self.get("items") if d.get(field)]
bundle_ids = {}
if ids:
for bundle in frappe.get_all(
doctype, filters={"name": ("in", ids)}, fields=["serial_and_batch_bundle", "name"]
):
bundle_ids[bundle.name] = bundle.serial_and_batch_bundle
if not bundle_ids:
return
for item in self.get("items"):
if item.get(field) and not item.serial_and_batch_bundle and bundle_ids.get(item.get(field)):
item.serial_and_batch_bundle = self.make_package_for_transfer(
bundle_ids.get(item.get(field)),
item.from_warehouse,
type_of_transaction="Outward",
do_not_submit=True,
)
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@@ -305,8 +337,7 @@ class BuyingController(SubcontractingController):
"posting_date": self.get("posting_date") or self.get("transation_date"), "posting_date": self.get("posting_date") or self.get("transation_date"),
"posting_time": posting_time, "posting_time": posting_time,
"qty": -1 * flt(d.get("stock_qty")), "qty": -1 * flt(d.get("stock_qty")),
"serial_no": d.get("serial_no"), "serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"batch_no": d.get("batch_no"),
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
@@ -463,7 +494,15 @@ class BuyingController(SubcontractingController):
sl_entries.append(from_warehouse_sle) sl_entries.append(from_warehouse_sle)
sle = self.get_sl_entries( sle = self.get_sl_entries(
d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()} d,
{
"actual_qty": flt(pr_qty),
"serial_and_batch_bundle": (
d.serial_and_batch_bundle
if not self.is_internal_transfer()
else self.get_package_for_target_warehouse(d)
),
},
) )
if self.is_return: if self.is_return:
@@ -471,7 +510,13 @@ class BuyingController(SubcontractingController):
self.doctype, self.name, d.item_code, self.return_against, item_row=d self.doctype, self.name, d.item_code, self.return_against, item_row=d
) )
sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1}) sle.update(
{
"outgoing_rate": outgoing_rate,
"recalculate_rate": 1,
"serial_and_batch_bundle": d.serial_and_batch_bundle,
}
)
if d.from_warehouse: if d.from_warehouse:
sle.dependant_sle_voucher_detail_no = d.name sle.dependant_sle_voucher_detail_no = d.name
else: else:
@@ -504,20 +549,30 @@ class BuyingController(SubcontractingController):
{ {
"warehouse": d.rejected_warehouse, "warehouse": d.rejected_warehouse,
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
"serial_no": cstr(d.rejected_serial_no).strip(),
"incoming_rate": 0.0, "incoming_rate": 0.0,
"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
}, },
) )
) )
if self.get("is_old_subcontracting_flow"): if self.get("is_old_subcontracting_flow"):
self.make_sl_entries_for_supplier_warehouse(sl_entries) self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries( self.make_sl_entries(
sl_entries, sl_entries,
allow_negative_stock=allow_negative_stock, allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher, via_landed_cost_voucher=via_landed_cost_voucher,
) )
def get_package_for_target_warehouse(self, item) -> str:
if not item.serial_and_batch_bundle:
return ""
return self.make_package_for_transfer(
item.serial_and_batch_bundle,
item.warehouse,
)
def update_ordered_and_reserved_qty(self): def update_ordered_and_reserved_qty(self):
po_map = {} po_map = {}
for d in self.get("items"): for d in self.get("items"):

View File

@@ -3,12 +3,13 @@
import json import json
from collections import defaultdict from collections import OrderedDict, defaultdict
import frappe import frappe
from frappe import scrub from frappe import scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.utils import nowdate, unique from frappe.query_builder.functions import Concat, Sum
from frappe.utils import nowdate, today, unique
import erpnext import erpnext
from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.stock.get_item_details import _get_item_tax_template
@@ -412,95 +413,136 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_batch_no(doctype, txt, searchfield, start, page_len, filters): def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
doctype = "Batch" doctype = "Batch"
cond = ""
if filters.get("posting_date"):
cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)"
batch_nos = None
args = {
"item_code": filters.get("item_code"),
"warehouse": filters.get("warehouse"),
"posting_date": filters.get("posting_date"),
"txt": "%{0}%".format(txt),
"start": start,
"page_len": page_len,
}
having_clause = "having sum(sle.actual_qty) > 0"
if filters.get("is_return"):
having_clause = ""
meta = frappe.get_meta(doctype, cached=True) meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields() searchfields = meta.get_search_fields()
search_columns = "" query = get_batches_from_stock_ledger_entries(searchfields, txt, filters)
search_cond = "" bundle_query = get_batches_from_serial_and_batch_bundle(searchfields, txt, filters)
if searchfields: data = (
search_columns = ", " + ", ".join(searchfields) frappe.qb.from_((query) + (bundle_query))
search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields]) .select("batch_no", "qty", "manufacturing_date", "expiry_date")
.offset(start)
if args.get("warehouse"): .limit(page_len)
searchfields = ["batch." + field for field in searchfields]
if searchfields:
search_columns = ", " + ", ".join(searchfields)
search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
batch_nos = frappe.db.sql(
"""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom,
concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date)
{search_columns}
from `tabStock Ledger Entry` sle
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where
batch.disabled = 0
and sle.is_cancelled = 0
and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s
or batch.expiry_date like %(txt)s
or batch.manufacturing_date like %(txt)s
{search_cond})
and batch.docstatus < 2
{cond}
{match_conditions}
group by batch_no {having_clause}
order by batch.expiry_date, sle.batch_no desc
limit %(page_len)s offset %(start)s""".format(
search_columns=search_columns,
cond=cond,
match_conditions=get_match_cond(doctype),
having_clause=having_clause,
search_cond=search_cond,
),
args,
) )
return batch_nos for field in searchfields:
data = data.select(field)
data = data.run()
data = get_filterd_batches(data)
return data
def get_filterd_batches(data):
batches = OrderedDict()
for batch_data in data:
if batch_data[0] not in batches:
batches[batch_data[0]] = list(batch_data)
else: else:
return frappe.db.sql( batches[batch_data[0]][1] += batch_data[1]
"""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date)
{search_columns}
from `tabBatch` batch
where batch.disabled = 0
and item = %(item_code)s
and (name like %(txt)s
or expiry_date like %(txt)s
or manufacturing_date like %(txt)s
{search_cond})
and docstatus < 2
{0}
{match_conditions}
order by expiry_date, name desc filterd_batch = []
limit %(page_len)s offset %(start)s""".format( for batch, batch_data in batches.items():
cond, if batch_data[1] > 0:
search_columns=search_columns, filterd_batch.append(tuple(batch_data))
search_cond=search_cond,
match_conditions=get_match_cond(doctype), return filterd_batch
),
args,
def get_batches_from_stock_ledger_entries(searchfields, txt, filters):
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_table = frappe.qb.DocType("Batch")
expiry_date = filters.get("posting_date") or today()
query = (
frappe.qb.from_(stock_ledger_entry)
.inner_join(batch_table)
.on(batch_table.name == stock_ledger_entry.batch_no)
.select(
stock_ledger_entry.batch_no,
Sum(stock_ledger_entry.actual_qty).as_("qty"),
) )
.where(((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())))
.where(stock_ledger_entry.is_cancelled == 0)
.where(
(stock_ledger_entry.item_code == filters.get("item_code"))
& (batch_table.disabled == 0)
& (stock_ledger_entry.batch_no.isnotnull())
)
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
)
query = query.select(
Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"),
Concat("EXP-", batch_table.expiry_date).as_("expiry_date"),
)
if filters.get("warehouse"):
query = query.where(stock_ledger_entry.warehouse == filters.get("warehouse"))
for field in searchfields:
query = query.select(batch_table[field])
if txt:
txt_condition = batch_table.name.like(txt)
for field in searchfields + ["name"]:
txt_condition |= batch_table[field].like(txt)
query = query.where(txt_condition)
return query
def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters):
bundle = frappe.qb.DocType("Serial and Batch Entry")
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_table = frappe.qb.DocType("Batch")
expiry_date = filters.get("posting_date") or today()
bundle_query = (
frappe.qb.from_(bundle)
.inner_join(stock_ledger_entry)
.on(bundle.parent == stock_ledger_entry.serial_and_batch_bundle)
.inner_join(batch_table)
.on(batch_table.name == bundle.batch_no)
.select(
bundle.batch_no,
Sum(bundle.qty).as_("qty"),
)
.where(((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())))
.where(stock_ledger_entry.is_cancelled == 0)
.where(
(stock_ledger_entry.item_code == filters.get("item_code"))
& (batch_table.disabled == 0)
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
)
.groupby(bundle.batch_no, bundle.warehouse)
)
bundle_query = bundle_query.select(
Concat("MFG-", batch_table.manufacturing_date),
Concat("EXP-", batch_table.expiry_date),
)
if filters.get("warehouse"):
bundle_query = bundle_query.where(stock_ledger_entry.warehouse == filters.get("warehouse"))
for field in searchfields:
bundle_query = bundle_query.select(batch_table[field])
if txt:
txt_condition = batch_table.name.like(txt)
for field in searchfields + ["name"]:
txt_condition |= batch_table[field].like(txt)
bundle_query = bundle_query.where(txt_condition)
return bundle_query
@frappe.whitelist() @frappe.whitelist()

View File

@@ -323,8 +323,6 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
def make_return_doc(doctype: str, source_name: str, target_doc=None): def make_return_doc(doctype: str, source_name: str, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
company = frappe.db.get_value("Delivery Note", source_name, "company") company = frappe.db.get_value("Delivery Note", source_name, "company")
default_warehouse_for_sales_return = frappe.get_cached_value( default_warehouse_for_sales_return = frappe.get_cached_value(
"Company", company, "default_warehouse_for_sales_return" "Company", company, "default_warehouse_for_sales_return"
@@ -392,23 +390,69 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
doc.run_method("calculate_taxes_and_totals") doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent): def update_item(source_doc, target_doc, source_parent):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
target_doc.qty = -1 * source_doc.qty target_doc.qty = -1 * source_doc.qty
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
if source_doc.serial_no: returned_serial_nos = []
if source_doc.get("serial_and_batch_bundle"):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent) returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos))
if serial_nos:
target_doc.serial_no = "\n".join(serial_nos)
if source_doc.get("rejected_serial_no"): type_of_transaction = "Inward"
if (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
}
)
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
if source_doc.get("rejected_serial_and_batch_bundle"):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos( returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field="rejected_serial_no" source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle"
) )
rejected_serial_nos = list(
set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos) type_of_transaction = "Inward"
if (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction"
) )
if rejected_serial_nos: == "Inward"
target_doc.rejected_serial_no = "\n".join(rejected_serial_nos) ):
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
}
)
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row( returned_qty_map = get_returned_qty_map_for_row(
@@ -573,8 +617,7 @@ def get_rate_for_return(
"posting_date": sle.get("posting_date"), "posting_date": sle.get("posting_date"),
"posting_time": sle.get("posting_time"), "posting_time": sle.get("posting_time"),
"qty": sle.actual_qty, "qty": sle.actual_qty,
"serial_no": sle.get("serial_no"), "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
"batch_no": sle.get("batch_no"),
"company": sle.company, "company": sle.company,
"voucher_type": sle.voucher_type, "voucher_type": sle.voucher_type,
"voucher_no": sle.voucher_no, "voucher_no": sle.voucher_no,
@@ -620,8 +663,20 @@ def get_filters(
return filters return filters
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"): def get_returned_serial_nos(
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None
):
from erpnext.stock.doctype.serial_no.serial_no import (
get_serial_nos as get_serial_nos_from_serial_no,
)
from erpnext.stock.serial_batch_bundle import get_serial_nos
if not serial_no_field:
serial_no_field = "serial_and_batch_bundle"
old_field = "serial_no"
if serial_no_field == "rejected_serial_and_batch_bundle":
old_field = "rejected_serial_no"
return_ref_field = frappe.scrub(child_doc.doctype) return_ref_field = frappe.scrub(child_doc.doctype)
if child_doc.doctype == "Delivery Note Item": if child_doc.doctype == "Delivery Note Item":
@@ -629,7 +684,10 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
serial_nos = [] serial_nos = []
fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"] fields = [
f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`",
f"`{'tab' + child_doc.doctype}`.`{old_field}`",
]
filters = [ filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "return_against", "=", parent_doc.name],
@@ -638,7 +696,16 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
[parent_doc.doctype, "docstatus", "=", 1], [parent_doc.doctype, "docstatus", "=", 1],
] ]
# Required for POS Invoice
if ignore_voucher_detail_no:
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
ids = []
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
serial_nos.extend(get_serial_nos(row.get(serial_no_field))) ids.append(row.get("serial_and_batch_bundle"))
if row.get(old_field):
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
serial_nos.extend(get_serial_nos(ids))
return serial_nos return serial_nos

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _, bold, throw from frappe import _, bold, throw
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime from frappe.utils import cint, flt, get_link_to_form, nowtime
from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
@@ -38,6 +38,9 @@ class SellingController(StockController):
self.validate_for_duplicate_items() self.validate_for_duplicate_items()
self.validate_target_warehouse() self.validate_target_warehouse()
self.validate_auto_repeat_subscription_dates() self.validate_auto_repeat_subscription_dates()
for table_field in ["items", "packed_items"]:
if self.get(table_field):
self.set_serial_and_batch_bundle(table_field)
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
@@ -299,8 +302,8 @@ class SellingController(StockController):
"item_code": p.item_code, "item_code": p.item_code,
"qty": flt(p.qty), "qty": flt(p.qty),
"uom": p.uom, "uom": p.uom,
"batch_no": cstr(p.batch_no).strip(), "serial_and_batch_bundle": p.serial_and_batch_bundle
"serial_no": cstr(p.serial_no).strip(), or get_serial_and_batch_bundle(p, self),
"name": d.name, "name": d.name,
"target_warehouse": p.target_warehouse, "target_warehouse": p.target_warehouse,
"company": self.company, "company": self.company,
@@ -323,8 +326,7 @@ class SellingController(StockController):
"uom": d.uom, "uom": d.uom,
"stock_uom": d.stock_uom, "stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor, "conversion_factor": d.conversion_factor,
"batch_no": cstr(d.get("batch_no")).strip(), "serial_and_batch_bundle": d.serial_and_batch_bundle,
"serial_no": cstr(d.get("serial_no")).strip(),
"name": d.name, "name": d.name,
"target_warehouse": d.target_warehouse, "target_warehouse": d.target_warehouse,
"company": self.company, "company": self.company,
@@ -337,6 +339,7 @@ class SellingController(StockController):
} }
) )
) )
return il return il
def has_product_bundle(self, item_code): def has_product_bundle(self, item_code):
@@ -427,8 +430,7 @@ class SellingController(StockController):
"posting_date": self.get("posting_date") or self.get("transaction_date"), "posting_date": self.get("posting_date") or self.get("transaction_date"),
"posting_time": self.get("posting_time") or nowtime(), "posting_time": self.get("posting_time") or nowtime(),
"qty": qty if cint(self.get("is_return")) else (-1 * qty), "qty": qty if cint(self.get("is_return")) else (-1 * qty),
"serial_no": d.get("serial_no"), "serial_and_batch_bundle": d.serial_and_batch_bundle,
"batch_no": d.get("batch_no"),
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
@@ -511,6 +513,7 @@ class SellingController(StockController):
"actual_qty": -1 * flt(item_row.qty), "actual_qty": -1 * flt(item_row.qty),
"incoming_rate": item_row.incoming_rate, "incoming_rate": item_row.incoming_rate,
"recalculate_rate": cint(self.is_return), "recalculate_rate": cint(self.is_return),
"serial_and_batch_bundle": item_row.serial_and_batch_bundle,
}, },
) )
if item_row.target_warehouse and not cint(self.is_return): if item_row.target_warehouse and not cint(self.is_return):
@@ -531,6 +534,11 @@ class SellingController(StockController):
if item_row.warehouse: if item_row.warehouse:
sle.dependant_sle_voucher_detail_no = item_row.name sle.dependant_sle_voucher_detail_no = item_row.name
if item_row.serial_and_batch_bundle:
sle["serial_and_batch_bundle"] = self.make_package_for_transfer(
item_row.serial_and_batch_bundle, item_row.target_warehouse
)
return sle return sle
def set_po_nos(self, for_validate=False): def set_po_nos(self, for_validate=False):
@@ -669,3 +677,40 @@ def set_default_income_account_for_item(obj):
if d.item_code: if d.item_code:
if getattr(d, "income_account", None): if getattr(d, "income_account", None):
set_item_default(d.item_code, obj.company, "income_account", d.income_account) set_item_default(d.item_code, obj.company, "income_account", d.income_account)
def get_serial_and_batch_bundle(child, parent):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if not frappe.db.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
):
return
item_details = frappe.db.get_value(
"Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
)
if not item_details.has_serial_no and not item_details.has_batch_no:
return
sn_doc = SerialBatchCreation(
{
"item_code": child.item_code,
"warehouse": child.warehouse,
"voucher_type": parent.doctype,
"voucher_no": parent.name,
"voucher_detail_no": child.name,
"posting_date": parent.posting_date,
"posting_time": parent.posting_time,
"qty": child.qty,
"type_of_transaction": "Outward" if child.qty > 0 else "Inward",
"company": parent.company,
"do_not_submit": "True",
}
)
doc = sn_doc.make_serial_and_batch_bundle()
child.db_set("serial_and_batch_bundle", doc.name)
return doc.name

View File

@@ -7,7 +7,7 @@ from typing import List, Tuple
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate from frappe.utils import cint, flt, get_link_to_form, getdate
import erpnext import erpnext
from erpnext.accounts.general_ledger import ( from erpnext.accounts.general_ledger import (
@@ -325,29 +325,6 @@ class StockController(AccountsController):
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
return stock_ledger return stock_ledger
def make_batches(self, warehouse_field):
"""Create batches if required. Called before submit"""
for d in self.items:
if d.get(warehouse_field) and not d.batch_no:
has_batch_no, create_new_batch = frappe.get_cached_value(
"Item", d.item_code, ["has_batch_no", "create_new_batch"]
)
if has_batch_no and create_new_batch:
d.batch_no = (
frappe.get_doc(
dict(
doctype="Batch",
item=d.item_code,
supplier=getattr(self, "supplier", None),
reference_doctype=self.doctype,
reference_name=self.name,
)
)
.insert()
.name
)
def check_expense_account(self, item): def check_expense_account(self, item):
if not item.get("expense_account"): if not item.get("expense_account"):
msg = _("Please set an Expense Account in the Items table") msg = _("Please set an Expense Account in the Items table")
@@ -387,27 +364,73 @@ class StockController(AccountsController):
) )
def delete_auto_created_batches(self): def delete_auto_created_batches(self):
for d in self.items: for row in self.items:
if not d.batch_no: if row.serial_and_batch_bundle:
continue
frappe.db.set_value( frappe.db.set_value(
"Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None "Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
) )
d.batch_no = None row.db_set("serial_and_batch_bundle", None)
d.db_set("batch_no", None)
for data in frappe.get_all( def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
"Batch", {"reference_name": self.name, "reference_doctype": self.doctype} if not table_name:
table_name = "items"
QTY_FIELD = {
"serial_and_batch_bundle": "qty",
"current_serial_and_batch_bundle": "current_qty",
"rejected_serial_and_batch_bundle": "rejected_qty",
}
for row in self.get(table_name):
for field in [
"serial_and_batch_bundle",
"current_serial_and_batch_bundle",
"rejected_serial_and_batch_bundle",
]:
if row.get(field):
frappe.get_doc("Serial and Batch Bundle", row.get(field)).set_serial_and_batch_values(
self, row, qty_field=QTY_FIELD[field]
)
def make_package_for_transfer(
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
): ):
frappe.delete_doc("Batch", data.name) bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
if not type_of_transaction:
type_of_transaction = "Inward"
bundle_doc = frappe.copy_doc(bundle_doc)
bundle_doc.warehouse = warehouse
bundle_doc.type_of_transaction = type_of_transaction
bundle_doc.voucher_type = self.doctype
bundle_doc.voucher_no = self.name
bundle_doc.is_cancelled = 0
for row in bundle_doc.entries:
row.is_outward = 0
row.qty = abs(row.qty)
row.stock_value_difference = abs(row.stock_value_difference)
if type_of_transaction == "Outward":
row.qty *= -1
row.stock_value_difference *= row.stock_value_difference
row.is_outward = 1
row.warehouse = warehouse
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.save(ignore_permissions=True)
return bundle_doc.name
def get_sl_entries(self, d, args): def get_sl_entries(self, d, args):
sl_dict = frappe._dict( sl_dict = frappe._dict(
{ {
"item_code": d.get("item_code", None), "item_code": d.get("item_code", None),
"warehouse": d.get("warehouse", None), "warehouse": d.get("warehouse", None),
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"fiscal_year": get_fiscal_year(self.posting_date, company=self.company)[0], "fiscal_year": get_fiscal_year(self.posting_date, company=self.company)[0],
@@ -420,8 +443,6 @@ class StockController(AccountsController):
), ),
"incoming_rate": 0, "incoming_rate": 0,
"company": self.company, "company": self.company,
"batch_no": cstr(d.get("batch_no")).strip(),
"serial_no": d.get("serial_no"),
"project": d.get("project") or self.get("project"), "project": d.get("project") or self.get("project"),
"is_cancelled": 1 if self.docstatus == 2 else 0, "is_cancelled": 1 if self.docstatus == 2 else 0,
} }

View File

@@ -8,10 +8,14 @@ from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, get_link_to_form from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_voucher_wise_serial_batch_from_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation, get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@@ -169,7 +173,11 @@ class SubcontractingController(StockController):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_transferred_items(self): def __get_transferred_items(self):
fields = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"] fields = [
f"`tabStock Entry`.`{self.subcontract_data.order_field}`",
"`tabStock Entry`.`name` as voucher_no",
]
alias_dict = { alias_dict = {
"item_code": "rm_item_code", "item_code": "rm_item_code",
"subcontracted_item": "main_item_code", "subcontracted_item": "main_item_code",
@@ -184,6 +192,7 @@ class SubcontractingController(StockController):
"basic_rate", "basic_rate",
"amount", "amount",
"serial_no", "serial_no",
"serial_and_batch_bundle",
"uom", "uom",
"subcontracted_item", "subcontracted_item",
"stock_uom", "stock_uom",
@@ -234,9 +243,11 @@ class SubcontractingController(StockController):
"serial_no", "serial_no",
"rm_item_code", "rm_item_code",
"reference_name", "reference_name",
"serial_and_batch_bundle",
"batch_no", "batch_no",
"consumed_qty", "consumed_qty",
"main_item_code", "main_item_code",
"parent as voucher_no",
], ],
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype}, filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
) )
@@ -253,6 +264,13 @@ class SubcontractingController(StockController):
} }
consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys()) consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys())
voucher_nos = [d.voucher_no for d in consumed_materials if d.voucher_no]
voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle(
voucher_no=voucher_nos,
is_outward=1,
get_subcontracted_item=("Subcontracting Receipt Supplied Item", "main_item_code"),
)
if return_consumed_items: if return_consumed_items:
return (consumed_materials, receipt_items) return (consumed_materials, receipt_items)
@@ -262,11 +280,29 @@ class SubcontractingController(StockController):
continue continue
self.available_materials[key]["qty"] -= row.consumed_qty self.available_materials[key]["qty"] -= row.consumed_qty
bundle_key = (row.rm_item_code, row.main_item_code, self.supplier_warehouse, row.voucher_no)
consumed_bundles = voucher_bundle_data.get(bundle_key, frappe._dict())
if consumed_bundles.serial_nos:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(consumed_bundles.serial_nos)
)
if consumed_bundles.batch_nos:
for batch_no, qty in consumed_bundles.batch_nos.items():
if qty:
# Conumed qty is negative therefore added it instead of subtracting
self.available_materials[key]["batch_no"][batch_no] += qty
consumed_bundles.batch_nos[batch_no] += abs(qty)
# Will be deprecated in v16
if row.serial_no: if row.serial_no:
self.available_materials[key]["serial_no"] = list( self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no)) set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
) )
# Will be deprecated in v16
if row.batch_no: if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
@@ -281,7 +317,16 @@ class SubcontractingController(StockController):
if not self.subcontract_orders: if not self.subcontract_orders:
return return
for row in self.__get_transferred_items(): transferred_items = self.__get_transferred_items()
voucher_nos = [row.voucher_no for row in transferred_items]
voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle(
voucher_no=voucher_nos,
is_outward=0,
get_subcontracted_item=("Stock Entry Detail", "subcontracted_item"),
)
for row in transferred_items:
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field)) key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if key not in self.available_materials: if key not in self.available_materials:
@@ -310,6 +355,20 @@ class SubcontractingController(StockController):
if row.batch_no: if row.batch_no:
details.batch_no[row.batch_no] += row.qty details.batch_no[row.batch_no] += row.qty
if voucher_bundle_data:
bundle_key = (row.rm_item_code, row.main_item_code, row.t_warehouse, row.voucher_no)
bundle_data = voucher_bundle_data.get(bundle_key, frappe._dict())
if bundle_data.serial_nos:
details.serial_no.extend(bundle_data.serial_nos)
bundle_data.serial_nos = []
if bundle_data.batch_nos:
for batch_no, qty in bundle_data.batch_nos.items():
if qty > 0:
details.batch_no[batch_no] += qty
bundle_data.batch_nos[batch_no] -= qty
self.__set_alternative_item_details(row) self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials) self.__transferred_items = copy.deepcopy(self.available_materials)
@@ -327,6 +386,7 @@ class SubcontractingController(StockController):
self.set(self.raw_material_table, []) self.set(self.raw_material_table, [])
for item in self._doc_before_save.supplied_items: for item in self._doc_before_save.supplied_items:
if item.reference_name in self.__changed_name: if item.reference_name in self.__changed_name:
self.__remove_serial_and_batch_bundle(item)
continue continue
if item.reference_name not in self.__reference_name: if item.reference_name not in self.__reference_name:
@@ -337,6 +397,10 @@ class SubcontractingController(StockController):
i += 1 i += 1
def __remove_serial_and_batch_bundle(self, item):
if item.serial_and_batch_bundle:
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
@@ -377,68 +441,89 @@ class SubcontractingController(StockController):
if self.alternative_item_details.get(bom_item.rm_item_code): if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __set_serial_nos(self, item_row, rm_obj): def __set_serial_and_batch_bundle(self, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field)) key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]: if not self.available_materials.get(key):
used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
rm_obj.serial_no = "\n".join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]["serial_no"].remove(sn)
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update(
{
"consumed_qty": qty,
"batch_no": batch_no,
"required_qty": qty,
self.subcontract_data.order_field: item_row.get(self.subcontract_data.order_field),
}
)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
if batch_qty >= qty or (
rm_obj.consumed_qty == 0
and self.backflush_based_on == "BOM"
and len(self.available_materials[key]["batch_no"]) == 1
):
if rm_obj.consumed_qty == 0:
self.__set_consumed_qty(rm_obj, qty)
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]["batch_no"][batch_no] -= qty
return return
elif qty > 0 and batch_qty > 0: if (
qty -= batch_qty not self.available_materials[key]["serial_no"] and not self.available_materials[key]["batch_no"]
new_rm_obj = self.append(self.raw_material_table, bom_item) ):
new_rm_obj.reference_name = item_row.name return
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]["batch_no"][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj: serial_nos = []
self.__set_consumed_qty(rm_obj, qty) batches = frappe._dict({})
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
serial_nos = self.__get_serial_nos_for_bundle(qty, key)
elif self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
batches = self.__get_batch_nos_for_bundle(qty, key)
bundle = SerialBatchCreation(
frappe._dict(
{
"company": self.company,
"item_code": rm_obj.rm_item_code,
"warehouse": self.supplier_warehouse,
"qty": qty,
"serial_nos": serial_nos,
"batches": batches,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": "Subcontracting Receipt",
"do_not_submit": True,
"type_of_transaction": "Outward" if qty > 0 else "Inward",
}
)
).make_serial_and_batch_bundle()
return bundle.name
def __get_batch_nos_for_bundle(self, qty, key):
available_batches = defaultdict(float)
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
qty_to_consumed = 0
if qty > 0:
if batch_qty >= qty:
qty_to_consumed = qty
else: else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) qty_to_consumed = batch_qty
self.__set_serial_nos(item_row, rm_obj)
qty -= qty_to_consumed
if qty_to_consumed > 0:
available_batches[batch_no] += qty_to_consumed
self.available_materials[key]["batch_no"][batch_no] -= qty_to_consumed
return available_batches
def __get_serial_nos_for_bundle(self, qty, key):
available_sns = sorted(self.available_materials[key]["serial_no"])[0 : cint(qty)]
serial_nos = []
for serial_no in available_sns:
serial_nos.append(serial_no)
self.available_materials[key]["serial_no"].remove(serial_no)
return serial_nos
def __add_supplied_item(self, item_row, bom_item, qty): def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item) rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name rm_obj.reference_name = item_row.name
if self.doctype == self.subcontract_data.order_doctype:
rm_obj.required_qty = qty
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
else:
rm_obj.consumed_qty = qty
rm_obj.required_qty = bom_item.required_qty or qty
setattr(
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
)
if self.doctype == "Subcontracting Receipt": if self.doctype == "Subcontracting Receipt":
args = frappe._dict( args = frappe._dict(
{ {
@@ -447,25 +532,23 @@ class SubcontractingController(StockController):
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty), "qty": -1 * flt(rm_obj.consumed_qty),
"serial_no": rm_obj.serial_no, "actual_qty": -1 * flt(rm_obj.consumed_qty),
"batch_no": rm_obj.batch_no,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"voucher_detail_no": item_row.name,
"company": self.company, "company": self.company,
"allow_zero_valuation": 1, "allow_zero_valuation": 1,
} }
) )
rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
if self.doctype == self.subcontract_data.order_doctype: rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
rm_obj.required_qty = qty item_row, rm_obj, rm_obj.consumed_qty
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
else:
rm_obj.consumed_qty = 0
setattr(
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
) )
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if rm_obj.serial_and_batch_bundle:
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item): def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
@@ -520,6 +603,53 @@ class SubcontractingController(StockController):
(row.item_code, row.get(self.subcontract_data.order_field)) (row.item_code, row.get(self.subcontract_data.order_field))
] -= row.qty ] -= row.qty
def __modify_serial_and_batch_bundle(self):
if self.is_new():
return
if self.doctype != "Subcontracting Receipt":
return
for item_row in self.items:
if self.__changed_name and item_row.name in self.__changed_name:
continue
modified_data = self.__get_bundle_to_modify(item_row.name)
if modified_data:
serial_nos = []
batches = frappe._dict({})
key = (
modified_data.rm_item_code,
item_row.item_code,
item_row.get(self.subcontract_data.order_field),
)
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
serial_nos = self.__get_serial_nos_for_bundle(modified_data.consumed_qty, key)
elif self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
batches = self.__get_batch_nos_for_bundle(modified_data.consumed_qty, key)
SerialBatchCreation(
{
"item_code": modified_data.rm_item_code,
"warehouse": self.supplier_warehouse,
"serial_and_batch_bundle": modified_data.serial_and_batch_bundle,
"type_of_transaction": "Outward",
"serial_nos": serial_nos,
"batches": batches,
"qty": modified_data.consumed_qty * -1,
}
).update_serial_and_batch_entries()
def __get_bundle_to_modify(self, name):
for row in self.get("supplied_items"):
if row.reference_name == name and row.serial_and_batch_bundle:
if row.consumed_qty != abs(
frappe.get_cached_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty")
):
return row
def __prepare_supplied_items(self): def __prepare_supplied_items(self):
self.initialized_fields() self.initialized_fields()
self.__get_subcontract_orders() self.__get_subcontract_orders()
@@ -527,6 +657,7 @@ class SubcontractingController(StockController):
self.get_available_materials() self.get_available_materials()
self.__remove_changed_rows() self.__remove_changed_rows()
self.__set_supplied_items() self.__set_supplied_items()
self.__modify_serial_and_batch_bundle()
def __validate_batch_no(self, row, key): def __validate_batch_no(self, row, key):
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
@@ -539,8 +670,8 @@ class SubcontractingController(StockController):
frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key): def __validate_serial_no(self, row, key):
if row.get("serial_no"): if row.get("serial_and_batch_bundle") and self.__transferred_items.get(key).get("serial_no"):
serial_nos = get_serial_nos(row.get("serial_no")) serial_nos = get_serial_nos_from_bundle(row.get("serial_and_batch_bundle"))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no")) incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
if incorrect_sn: if incorrect_sn:
@@ -667,9 +798,7 @@ class SubcontractingController(StockController):
scr_qty = flt(item.qty) * flt(item.conversion_factor) scr_qty = flt(item.qty) * flt(item.conversion_factor)
if scr_qty: if scr_qty:
sle = self.get_sl_entries( sle = self.get_sl_entries(item, {"actual_qty": flt(scr_qty)})
item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()}
)
rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9 rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9
incoming_rate = flt(item.rate, rate_db_precision) incoming_rate = flt(item.rate, rate_db_precision)
sle.update( sle.update(
@@ -687,7 +816,6 @@ class SubcontractingController(StockController):
{ {
"warehouse": item.rejected_warehouse, "warehouse": item.rejected_warehouse,
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor), "actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
"serial_no": cstr(item.rejected_serial_no).strip(),
"incoming_rate": 0.0, "incoming_rate": 0.0,
}, },
) )
@@ -716,8 +844,7 @@ class SubcontractingController(StockController):
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"qty": -1 * item.consumed_qty, "qty": -1 * item.consumed_qty,
"serial_no": item.serial_no, "serial_and_batch_bundle": item.serial_and_batch_bundle,
"batch_no": item.batch_no,
} }
) )
@@ -865,7 +992,6 @@ def make_rm_stock_entry(
if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code: if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code:
rm_item_code = rm_item.get("rm_item_code") rm_item_code = rm_item.get("rm_item_code")
items_dict = { items_dict = {
rm_item_code: { rm_item_code: {
rm_detail_field: rm_item.get("name"), rm_detail_field: rm_item.get("name"),
@@ -877,8 +1003,7 @@ def make_rm_stock_entry(
"from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"), "from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"),
"to_warehouse": subcontract_order.supplier_warehouse, "to_warehouse": subcontract_order.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"), "stock_uom": rm_item.get("stock_uom"),
"serial_no": rm_item.get("serial_no"), "serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"batch_no": rm_item.get("batch_no"),
"main_item_code": fg_item_code, "main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
} }
@@ -953,7 +1078,6 @@ def make_return_stock_entry_for_subcontract(
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field) add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
ste_doc.set_stock_entry_type() ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
return ste_doc return ste_doc

View File

@@ -15,6 +15,11 @@ from erpnext.controllers.subcontracting_controller import (
) )
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
@@ -311,9 +316,6 @@ class TestSubcontractingController(FrappeTestCase):
scr1 = make_subcontracting_receipt(sco.name) scr1 = make_subcontracting_receipt(sco.name)
scr1.save() scr1.save()
scr1.supplied_items[0].consumed_qty = 5 scr1.supplied_items[0].consumed_qty = 5
scr1.supplied_items[0].serial_no = "\n".join(
sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5])
)
scr1.submit() scr1.submit()
for key, value in get_supplied_items(scr1).items(): for key, value in get_supplied_items(scr1).items():
@@ -341,6 +343,7 @@ class TestSubcontractingController(FrappeTestCase):
- Create the 3 SCR against the SCO and split Subcontracted Items into two batches. - Create the 3 SCR against the SCO and split Subcontracted Items into two batches.
- Keep the qty as 2 for Subcontracted Item in the SCR. - Keep the qty as 2 for Subcontracted Item in the SCR.
""" """
from erpnext.stock.serial_batch_bundle import get_batch_nos
set_backflush_based_on("BOM") set_backflush_based_on("BOM")
service_items = [ service_items = [
@@ -426,6 +429,7 @@ class TestSubcontractingController(FrappeTestCase):
for key, value in get_supplied_items(scr1).items(): for key, value in get_supplied_items(scr1).items():
self.assertEqual(value.qty, 4) self.assertEqual(value.qty, 4)
frappe.flags.add_debugger = True
scr2 = make_subcontracting_receipt(sco.name) scr2 = make_subcontracting_receipt(sco.name)
scr2.items[0].qty = 2 scr2.items[0].qty = 2
add_second_row_in_scr(scr2) add_second_row_in_scr(scr2)
@@ -612,9 +616,6 @@ class TestSubcontractingController(FrappeTestCase):
scr1.load_from_db() scr1.load_from_db()
scr1.supplied_items[0].consumed_qty = 5 scr1.supplied_items[0].consumed_qty = 5
scr1.supplied_items[0].serial_no = "\n".join(
itemwise_details[scr1.supplied_items[0].rm_item_code]["serial_no"]
)
scr1.save() scr1.save()
scr1.submit() scr1.submit()
@@ -651,6 +652,16 @@ class TestSubcontractingController(FrappeTestCase):
- System should throw the error and not allowed to save the SCR. - System should throw the error and not allowed to save the SCR.
""" """
serial_no = "ABC"
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"item_code": "Subcontracted SRM Item 2",
"serial_no": serial_no,
}
).insert()
set_backflush_based_on("Material Transferred for Subcontract") set_backflush_based_on("Material Transferred for Subcontract")
service_items = [ service_items = [
{ {
@@ -677,10 +688,39 @@ class TestSubcontractingController(FrappeTestCase):
scr1 = make_subcontracting_receipt(sco.name) scr1 = make_subcontracting_receipt(sco.name)
scr1.save() scr1.save()
scr1.supplied_items[0].serial_no = "ABCD" bundle = frappe.get_doc(
"Serial and Batch Bundle", scr1.supplied_items[0].serial_and_batch_bundle
)
original_serial_no = ""
for row in bundle.entries:
if row.idx == 1:
original_serial_no = row.serial_no
row.serial_no = "ABC"
break
bundle.save()
self.assertRaises(frappe.ValidationError, scr1.save) self.assertRaises(frappe.ValidationError, scr1.save)
bundle.load_from_db()
for row in bundle.entries:
if row.idx == 1:
row.serial_no = original_serial_no
break
bundle.save()
scr1.load_from_db()
scr1.save()
self.delete_bundle_from_scr(scr1)
scr1.delete() scr1.delete()
@staticmethod
def delete_bundle_from_scr(scr):
for row in scr.supplied_items:
if not row.serial_and_batch_bundle:
continue
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
def test_partial_transfer_batch_based_on_material_transfer(self): def test_partial_transfer_batch_based_on_material_transfer(self):
""" """
- Set backflush based on Material Transferred for Subcontract. - Set backflush based on Material Transferred for Subcontract.
@@ -724,12 +764,9 @@ class TestSubcontractingController(FrappeTestCase):
for key, value in get_supplied_items(scr1).items(): for key, value in get_supplied_items(scr1).items():
details = itemwise_details.get(key) details = itemwise_details.get(key)
self.assertEqual(value.qty, 3) self.assertEqual(value.qty, 3)
transferred_batch_no = details.batch_no
self.assertEqual(value.batch_no, details.batch_no)
scr1.load_from_db() scr1.load_from_db()
scr1.supplied_items[0].consumed_qty = 5 scr1.supplied_items[0].consumed_qty = 5
scr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
scr1.save() scr1.save()
scr1.submit() scr1.submit()
@@ -883,6 +920,15 @@ def update_item_details(child_row, details):
if child_row.batch_no: if child_row.batch_no:
details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty")
if child_row.serial_and_batch_bundle:
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
for row in doc.get("entries"):
if row.serial_no:
details.serial_no.append(row.serial_no)
if row.batch_no:
details.batch_no[row.batch_no] += row.qty * (-1 if doc.type_of_transaction == "Outward" else 1)
def make_stock_transfer_entry(**args): def make_stock_transfer_entry(**args):
args = frappe._dict(args) args = frappe._dict(args)
@@ -903,18 +949,35 @@ def make_stock_transfer_entry(**args):
item_details = args.itemwise_details.get(row.item_code) item_details = args.itemwise_details.get(row.item_code)
serial_nos = []
batches = defaultdict(float)
if item_details and item_details.serial_no: if item_details and item_details.serial_no:
serial_nos = item_details.serial_no[0 : cint(row.qty)] serial_nos = item_details.serial_no[0 : cint(row.qty)]
item["serial_no"] = "\n".join(serial_nos)
item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos)) item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
if item_details and item_details.batch_no: if item_details and item_details.batch_no:
for batch_no, batch_qty in item_details.batch_no.items(): for batch_no, batch_qty in item_details.batch_no.items():
if batch_qty >= row.qty: if batch_qty >= row.qty:
item["batch_no"] = batch_no batches[batch_no] = row.qty
item_details.batch_no[batch_no] -= row.qty item_details.batch_no[batch_no] -= row.qty
break break
if serial_nos or batches:
item["serial_and_batch_bundle"] = make_serial_batch_bundle(
frappe._dict(
{
"item_code": row.item_code,
"warehouse": row.warehouse or "_Test Warehouse - _TC",
"qty": (row.qty or 1) * -1,
"batches": batches,
"serial_nos": serial_nos,
"voucher_type": "Delivery Note",
"type_of_transaction": "Outward",
"do_not_submit": True,
}
)
).name
items.append(item) items.append(item)
ste_dict = make_rm_stock_entry(args.sco_no, items) ste_dict = make_rm_stock_entry(args.sco_no, items)
@@ -956,7 +1019,7 @@ def make_raw_materials():
"batch_number_series": "BAT.####", "batch_number_series": "BAT.####",
}, },
"Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
} }
for item, properties in raw_materials.items(): for item, properties in raw_materials.items():

View File

@@ -67,6 +67,12 @@ treeviews = [
"Department", "Department",
] ]
jinja = {
"methods": [
"erpnext.stock.serial_batch_bundle.get_serial_or_batch_nos",
],
}
# website # website
update_website_context = [ update_website_context = [
"erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.e_commerce.shopping_cart.utils.update_website_context",

View File

@@ -7,6 +7,19 @@ frappe.ui.form.on('Maintenance Schedule', {
frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer); frm.set_query('customer', erpnext.queries.customer);
frm.set_query('serial_and_batch_bundle', 'items', (doc, cdt, cdn) => {
let item = locals[cdt][cdn];
return {
filters: {
'item_code': item.item_code,
'voucher_type': 'Maintenance Schedule',
'type_of_transaction': 'Maintenance',
'company': doc.company,
}
}
});
}, },
onload: function (frm) { onload: function (frm) {
if (!frm.doc.status) { if (!frm.doc.status) {

View File

@@ -7,7 +7,6 @@ from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate
from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_valid_serial_nos
from erpnext.utilities.transaction_base import TransactionBase, delete_events from erpnext.utilities.transaction_base import TransactionBase, delete_events
@@ -74,8 +73,12 @@ class MaintenanceSchedule(TransactionBase):
email_map = {} email_map = {}
for d in self.get("items"): for d in self.get("items"):
if d.serial_no: if d.serial_and_batch_bundle:
serial_nos = get_valid_serial_nos(d.serial_no) serial_nos = frappe.get_doc(
"Serial and Batch Bundle", d.serial_and_batch_bundle
).get_serial_nos()
if serial_nos:
self.validate_serial_no(d.item_code, serial_nos, d.start_date) self.validate_serial_no(d.item_code, serial_nos, d.start_date)
self.update_amc_date(serial_nos, d.end_date) self.update_amc_date(serial_nos, d.end_date)
@@ -241,9 +244,27 @@ class MaintenanceSchedule(TransactionBase):
self.validate_maintenance_detail() self.validate_maintenance_detail()
self.validate_dates_with_periodicity() self.validate_dates_with_periodicity()
self.validate_sales_order() self.validate_sales_order()
self.validate_serial_no_bundle()
if not self.schedules or self.validate_items_table_change() or self.validate_no_of_visits(): if not self.schedules or self.validate_items_table_change() or self.validate_no_of_visits():
self.generate_schedule() self.generate_schedule()
def validate_serial_no_bundle(self):
ids = [d.serial_and_batch_bundle for d in self.items if d.serial_and_batch_bundle]
if not ids:
return
voucher_nos = frappe.get_all(
"Serial and Batch Bundle", fields=["name", "voucher_type"], filters={"name": ("in", ids)}
)
for row in voucher_nos:
if row.voucher_type != "Maintenance Schedule":
msg = f"""Serial and Batch Bundle {row.name}
should have voucher type as 'Maintenance Schedule'"""
frappe.throw(_(msg))
def on_update(self): def on_update(self):
self.db_set("status", "Draft") self.db_set("status", "Draft")
@@ -341,9 +362,14 @@ class MaintenanceSchedule(TransactionBase):
def on_cancel(self): def on_cancel(self):
for d in self.get("items"): for d in self.get("items"):
if d.serial_no: if d.serial_and_batch_bundle:
serial_nos = get_valid_serial_nos(d.serial_no) serial_nos = frappe.get_doc(
"Serial and Batch Bundle", d.serial_and_batch_bundle
).get_serial_nos()
if serial_nos:
self.update_amc_date(serial_nos) self.update_amc_date(serial_nos)
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
delete_events(self.doctype, self.name) delete_events(self.doctype, self.name)
@@ -397,7 +423,11 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
target.maintenance_schedule_detail = s_id target.maintenance_schedule_detail = s_id
def update_serial(source, target, parent): def update_serial(source, target, parent):
serial_nos = get_serial_nos(target.serial_no) if source.serial_and_batch_bundle:
serial_nos = frappe.get_doc(
"Serial and Batch Bundle", source.serial_and_batch_bundle
).get_serial_nos()
if len(serial_nos) == 1: if len(serial_nos) == 1:
target.serial_no = serial_nos[0] target.serial_no = serial_nos[0]
else: else:

View File

@@ -20,7 +20,9 @@
"sales_person", "sales_person",
"reference", "reference",
"serial_no", "serial_no",
"sales_order" "sales_order",
"column_break_ugqr",
"serial_and_batch_bundle"
], ],
"fields": [ "fields": [
{ {
@@ -121,7 +123,8 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Serial No", "label": "Serial No",
"oldfieldname": "serial_no", "oldfieldname": "serial_no",
"oldfieldtype": "Small Text" "oldfieldtype": "Small Text",
"read_only": 1
}, },
{ {
"fieldname": "sales_order", "fieldname": "sales_order",
@@ -144,17 +147,31 @@
{ {
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "column_break_ugqr",
"fieldtype": "Column Break"
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-04-15 16:09:47.311994", "modified": "2023-03-22 18:44:36.816037",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Schedule Item", "name": "Maintenance Schedule Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@@ -16,6 +16,7 @@
"production_item", "production_item",
"item_name", "item_name",
"for_quantity", "for_quantity",
"serial_and_batch_bundle",
"serial_no", "serial_no",
"column_break_12", "column_break_12",
"wip_warehouse", "wip_warehouse",
@@ -391,13 +392,17 @@
{ {
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Serial No" "hidden": 1,
"label": "Serial No",
"read_only": 1
}, },
{ {
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1,
"label": "Batch No", "label": "Batch No",
"options": "Batch" "options": "Batch",
"read_only": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -435,6 +440,14 @@
"fieldname": "expected_end_date", "fieldname": "expected_end_date",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Expected End Date" "label": "Expected End Date"
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,

View File

@@ -22,6 +22,11 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
) )
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -672,8 +677,11 @@ class TestWorkOrder(FrappeTestCase):
if row.is_finished_item: if row.is_finished_item:
self.assertEqual(row.item_code, fg_item) self.assertEqual(row.item_code, fg_item)
self.assertEqual(row.qty, 10) self.assertEqual(row.qty, 10)
self.assertTrue(row.batch_no in batches)
batches.remove(row.batch_no) bundle_id = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for bundle_row in bundle_id.get("entries"):
self.assertTrue(bundle_row.batch_no in batches)
batches.remove(bundle_row.batch_no)
ste1.submit() ste1.submit()
@@ -682,8 +690,12 @@ class TestWorkOrder(FrappeTestCase):
for row in ste1.get("items"): for row in ste1.get("items"):
if row.is_finished_item: if row.is_finished_item:
self.assertEqual(row.item_code, fg_item) self.assertEqual(row.item_code, fg_item)
self.assertEqual(row.qty, 10) self.assertEqual(row.qty, 20)
remaining_batches.append(row.batch_no)
bundle_id = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for bundle_row in bundle_id.get("entries"):
self.assertTrue(bundle_row.batch_no in batches)
remaining_batches.append(bundle_row.batch_no)
self.assertEqual(sorted(remaining_batches), sorted(batches)) self.assertEqual(sorted(remaining_batches), sorted(batches))
@@ -1168,18 +1180,28 @@ class TestWorkOrder(FrappeTestCase):
try: try:
wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True) wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
serial_nos = wo_order.serial_no serial_nos = self.get_serial_nos_for_fg(wo_order.name)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
stock_entry.set_work_order_details() stock_entry.set_work_order_details()
stock_entry.set_serial_no_batch_for_finished_good() stock_entry.set_serial_no_batch_for_finished_good()
for row in stock_entry.items: for row in stock_entry.items:
if row.item_code == fg_item: if row.item_code == fg_item:
self.assertTrue(row.serial_no) self.assertTrue(row.serial_and_batch_bundle)
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos))) self.assertEqual(
sorted(get_serial_nos_from_bundle(row.serial_and_batch_bundle)), sorted(serial_nos)
)
except frappe.MandatoryError: except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order") self.fail("Batch generation causing failing in Work Order")
def get_serial_nos_for_fg(self, work_order):
serial_nos = []
for row in frappe.get_all("Serial No", filters={"work_order": work_order}):
serial_nos.append(row.name)
return serial_nos
@change_settings( @change_settings(
"Manufacturing Settings", "Manufacturing Settings",
{"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"},
@@ -1272,63 +1294,66 @@ class TestWorkOrder(FrappeTestCase):
fg_item = "Test FG Item with Batch Raw Materials" fg_item = "Test FG Item with Batch Raw Materials"
ste_doc = test_stock_entry.make_stock_entry( ste_doc = test_stock_entry.make_stock_entry(
item_code=batch_item, target="Stores - _TC", qty=2, basic_rate=100, do_not_save=True item_code=batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
ste_doc.append(
"items",
{
"item_code": batch_item,
"item_name": batch_item,
"description": batch_item,
"basic_rate": 100,
"t_warehouse": "Stores - _TC",
"qty": 2,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1,
},
) )
# Inward raw materials in Stores warehouse # Inward raw materials in Stores warehouse
ste_doc.insert() ste_doc.insert()
ste_doc.submit() ste_doc.submit()
ste_doc.load_from_db()
batch_list = sorted([row.batch_no for row in ste_doc.items]) batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle)
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4) wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
transferred_ste_doc = frappe.get_doc( transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4) make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
) )
transferred_ste_doc.items[0].qty = 2 transferred_ste_doc.items[0].qty = 4
transferred_ste_doc.items[0].batch_no = batch_list[0] transferred_ste_doc.items[0].serial_and_batch_bundle = make_serial_batch_bundle(
frappe._dict(
{
"item_code": batch_item,
"warehouse": "Stores - _TC",
"company": transferred_ste_doc.company,
"qty": 4,
"voucher_type": "Stock Entry",
"batches": frappe._dict({batch_no: 4}),
"posting_date": transferred_ste_doc.posting_date,
"posting_time": transferred_ste_doc.posting_time,
"type_of_transaction": "Outward",
"do_not_submit": True,
}
)
).name
new_row = copy.deepcopy(transferred_ste_doc.items[0])
new_row.name = ""
new_row.batch_no = batch_list[1]
# Transferred two batches from Stores to WIP Warehouse
transferred_ste_doc.append("items", new_row)
transferred_ste_doc.submit() transferred_ste_doc.submit()
transferred_ste_doc.load_from_db()
# First Manufacture stock entry # First Manufacture stock entry
manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1)) manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1))
manufacture_ste_doc1.submit()
manufacture_ste_doc1.load_from_db()
# Batch no should be same as transferred Batch no # Batch no should be same as transferred Batch no
self.assertEqual(manufacture_ste_doc1.items[0].batch_no, batch_list[0]) self.assertEqual(
get_batch_from_bundle(manufacture_ste_doc1.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(manufacture_ste_doc1.items[0].qty, 1) self.assertEqual(manufacture_ste_doc1.items[0].qty, 1)
manufacture_ste_doc1.submit()
# Second Manufacture stock entry # Second Manufacture stock entry
manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2)) manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2))
manufacture_ste_doc2.submit()
manufacture_ste_doc2.load_from_db()
# Batch no should be same as transferred Batch no self.assertTrue(manufacture_ste_doc2.items[0].serial_and_batch_bundle)
self.assertEqual(manufacture_ste_doc2.items[0].batch_no, batch_list[0]) bundle_doc = frappe.get_doc(
self.assertEqual(manufacture_ste_doc2.items[0].qty, 1) "Serial and Batch Bundle", manufacture_ste_doc2.items[0].serial_and_batch_bundle
self.assertEqual(manufacture_ste_doc2.items[1].batch_no, batch_list[1]) )
self.assertEqual(manufacture_ste_doc2.items[1].qty, 1)
for d in bundle_doc.entries:
self.assertEqual(d.batch_no, batch_no)
self.assertEqual(abs(d.qty), 2)
def test_backflushed_serial_no_raw_materials_based_on_transferred(self): def test_backflushed_serial_no_raw_materials_based_on_transferred(self):
frappe.db.set_value( frappe.db.set_value(
@@ -1386,76 +1411,79 @@ class TestWorkOrder(FrappeTestCase):
fg_item = "Test FG Item with Serial & Batch No Raw Materials" fg_item = "Test FG Item with Serial & Batch No Raw Materials"
ste_doc = test_stock_entry.make_stock_entry( ste_doc = test_stock_entry.make_stock_entry(
item_code=sn_batch_item, target="Stores - _TC", qty=2, basic_rate=100, do_not_save=True item_code=sn_batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
ste_doc.append(
"items",
{
"item_code": sn_batch_item,
"item_name": sn_batch_item,
"description": sn_batch_item,
"basic_rate": 100,
"t_warehouse": "Stores - _TC",
"qty": 2,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1,
},
) )
# Inward raw materials in Stores warehouse # Inward raw materials in Stores warehouse
ste_doc.insert() ste_doc.insert()
ste_doc.submit() ste_doc.submit()
ste_doc.load_from_db()
batch_dict = {row.batch_no: get_serial_nos(row.serial_no) for row in ste_doc.items} serial_nos = []
batches = list(batch_dict.keys()) for row in ste_doc.items:
bundle_doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for d in bundle_doc.entries:
serial_nos.append(d.serial_no)
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4) wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
transferred_ste_doc = frappe.get_doc( transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4) make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
) )
transferred_ste_doc.items[0].qty = 2 transferred_ste_doc.items[0].qty = 4
transferred_ste_doc.items[0].batch_no = batches[0] transferred_ste_doc.items[0].serial_and_batch_bundle = make_serial_batch_bundle(
transferred_ste_doc.items[0].serial_no = "\n".join(batch_dict.get(batches[0])) frappe._dict(
{
"item_code": transferred_ste_doc.get("items")[0].item_code,
"warehouse": transferred_ste_doc.get("items")[0].s_warehouse,
"company": transferred_ste_doc.company,
"qty": 4,
"type_of_transaction": "Outward",
"voucher_type": "Stock Entry",
"serial_nos": serial_nos,
"posting_date": transferred_ste_doc.posting_date,
"posting_time": transferred_ste_doc.posting_time,
"do_not_submit": True,
}
)
).name
new_row = copy.deepcopy(transferred_ste_doc.items[0])
new_row.name = ""
new_row.batch_no = batches[1]
new_row.serial_no = "\n".join(batch_dict.get(batches[1]))
# Transferred two batches from Stores to WIP Warehouse
transferred_ste_doc.append("items", new_row)
transferred_ste_doc.submit() transferred_ste_doc.submit()
transferred_ste_doc.load_from_db()
# First Manufacture stock entry # First Manufacture stock entry
manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1)) manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1))
manufacture_ste_doc1.submit()
manufacture_ste_doc1.load_from_db()
# Batch no & Serial Nos should be same as transferred Batch no & Serial Nos # Batch no & Serial Nos should be same as transferred Batch no & Serial Nos
batch_no = manufacture_ste_doc1.items[0].batch_no bundle = manufacture_ste_doc1.items[0].serial_and_batch_bundle
self.assertEqual( self.assertTrue(bundle)
get_serial_nos(manufacture_ste_doc1.items[0].serial_no)[0], batch_dict.get(batch_no)[0]
)
self.assertEqual(manufacture_ste_doc1.items[0].qty, 1)
manufacture_ste_doc1.submit() bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle)
for d in bundle_doc.entries:
self.assertTrue(d.serial_no)
self.assertTrue(d.batch_no)
batch_no = frappe.get_cached_value("Serial No", d.serial_no, "batch_no")
self.assertEqual(d.batch_no, batch_no)
serial_nos.remove(d.serial_no)
# Second Manufacture stock entry # Second Manufacture stock entry
manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2)) manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 3))
manufacture_ste_doc2.submit()
manufacture_ste_doc2.load_from_db()
# Batch no & Serial Nos should be same as transferred Batch no & Serial Nos bundle = manufacture_ste_doc2.items[0].serial_and_batch_bundle
batch_no = manufacture_ste_doc2.items[0].batch_no self.assertTrue(bundle)
self.assertEqual(
get_serial_nos(manufacture_ste_doc2.items[0].serial_no)[0], batch_dict.get(batch_no)[1]
)
self.assertEqual(manufacture_ste_doc2.items[0].qty, 1)
batch_no = manufacture_ste_doc2.items[1].batch_no bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle)
self.assertEqual( for d in bundle_doc.entries:
get_serial_nos(manufacture_ste_doc2.items[1].serial_no)[0], batch_dict.get(batch_no)[0] self.assertTrue(d.serial_no)
) self.assertTrue(d.batch_no)
self.assertEqual(manufacture_ste_doc2.items[1].qty, 1) serial_nos.remove(d.serial_no)
self.assertFalse(serial_nos)
def test_non_consumed_material_return_against_work_order(self): def test_non_consumed_material_return_against_work_order(self):
frappe.db.set_value( frappe.db.set_value(
@@ -1490,13 +1518,10 @@ class TestWorkOrder(FrappeTestCase):
for row in ste_doc.items: for row in ste_doc.items:
row.qty += 2 row.qty += 2
row.transfer_qty += 2 row.transfer_qty += 2
nste_doc = test_stock_entry.make_stock_entry( test_stock_entry.make_stock_entry(
item_code=row.item_code, target="Stores - _TC", qty=row.qty, basic_rate=100 item_code=row.item_code, target="Stores - _TC", qty=row.qty, basic_rate=100
) )
row.batch_no = nste_doc.items[0].batch_no
row.serial_no = nste_doc.items[0].serial_no
ste_doc.save() ste_doc.save()
ste_doc.submit() ste_doc.submit()
ste_doc.load_from_db() ste_doc.load_from_db()
@@ -1508,9 +1533,19 @@ class TestWorkOrder(FrappeTestCase):
row.qty -= 2 row.qty -= 2
row.transfer_qty -= 2 row.transfer_qty -= 2
if row.serial_no: if not row.serial_and_batch_bundle:
serial_nos = get_serial_nos(row.serial_no) continue
row.serial_no = "\n".join(serial_nos[0:5])
bundle_id = row.serial_and_batch_bundle
bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle_id)
if bundle_doc.has_serial_no:
bundle_doc.set("entries", bundle_doc.entries[0:5])
else:
for bundle_row in bundle_doc.entries:
bundle_row.qty += 2
bundle_doc.save()
bundle_doc.load_from_db()
ste_doc.save() ste_doc.save()
ste_doc.submit() ste_doc.submit()

View File

@@ -42,7 +42,6 @@
"has_serial_no", "has_serial_no",
"has_batch_no", "has_batch_no",
"column_break_18", "column_break_18",
"serial_no",
"batch_size", "batch_size",
"required_items_section", "required_items_section",
"materials_and_operations_tab", "materials_and_operations_tab",
@@ -532,13 +531,6 @@
"label": "Has Batch No", "label": "Has Batch No",
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "has_serial_no",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial Nos",
"no_copy": 1
},
{ {
"default": "0", "default": "0",
"depends_on": "has_batch_no", "depends_on": "has_batch_no",

View File

@@ -17,6 +17,7 @@ from frappe.utils import (
get_datetime, get_datetime,
get_link_to_form, get_link_to_form,
getdate, getdate,
now,
nowdate, nowdate,
time_diff_in_hours, time_diff_in_hours,
) )
@@ -32,12 +33,7 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
) )
from erpnext.stock.doctype.batch.batch import make_batch from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
from erpnext.stock.doctype.serial_no.serial_no import ( from erpnext.stock.doctype.serial_no.serial_no import get_available_serial_nos, get_serial_nos
auto_make_serial_nos,
clean_serial_no_string,
get_auto_serial_nos,
get_serial_nos,
)
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company
from erpnext.utilities.transaction_base import validate_uom_is_integer from erpnext.utilities.transaction_base import validate_uom_is_integer
@@ -448,24 +444,53 @@ class WorkOrder(Document):
frappe.delete_doc("Batch", row.name) frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args): def make_serial_nos(self, args):
self.serial_no = clean_serial_no_string(self.serial_no) item_details = frappe.get_cached_value(
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") "Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
if serial_no_series:
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
if self.serial_no:
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
auto_make_serial_nos(args)
serial_nos_length = len(get_serial_nos(self.serial_no))
if serial_nos_length != self.qty:
frappe.throw(
_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(
self.qty, self.production_item, serial_nos_length
),
SerialNoQtyError,
) )
serial_nos = []
if item_details.serial_no_series:
serial_nos = get_available_serial_nos(item_details.serial_no_series, self.qty)
if not serial_nos:
return
fields = [
"name",
"serial_no",
"creation",
"modified",
"owner",
"modified_by",
"company",
"item_code",
"item_name",
"description",
"status",
"work_order",
]
serial_nos_details = []
for serial_no in serial_nos:
serial_nos_details.append(
(
serial_no,
serial_no,
now(),
now(),
frappe.session.user,
frappe.session.user,
self.company,
self.production_item,
item_details.item_name,
item_details.description,
"Inactive",
self.name,
)
)
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
def create_job_card(self): def create_job_card(self):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@@ -1042,24 +1067,6 @@ class WorkOrder(Document):
bom.set_bom_material_details() bom.set_bom_material_details()
return bom return bom
def update_batch_produced_qty(self, stock_entry_doc):
if not cint(
frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")
):
return
for row in stock_entry_doc.items:
if row.batch_no and (row.is_finished_item or row.is_scrap_item):
qty = frappe.get_all(
"Stock Entry Detail",
filters={"batch_no": row.batch_no, "docstatus": 1},
or_filters={"is_finished_item": 1, "is_scrap_item": 1},
fields=["sum(qty)"],
as_list=1,
)[0][0]
frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
@@ -1357,10 +1364,10 @@ def split_qty_based_on_batch_size(wo_doc, row, qty):
def get_serial_nos_for_job_card(row, wo_doc): def get_serial_nos_for_job_card(row, wo_doc):
if not wo_doc.serial_no: if not wo_doc.has_serial_no:
return return
serial_nos = get_serial_nos(wo_doc.serial_no) serial_nos = get_serial_nos_for_work_order(wo_doc.name, wo_doc.production_item)
used_serial_nos = [] used_serial_nos = []
for d in frappe.get_all( for d in frappe.get_all(
"Job Card", "Job Card",
@@ -1373,6 +1380,21 @@ def get_serial_nos_for_job_card(row, wo_doc):
row.serial_no = "\n".join(serial_nos[0 : cint(row.job_card_qty)]) row.serial_no = "\n".join(serial_nos[0 : cint(row.job_card_qty)])
def get_serial_nos_for_work_order(work_order, production_item):
serial_nos = []
for d in frappe.get_all(
"Serial No",
fields=["name"],
filters={
"work_order": work_order,
"item_code": production_item,
},
):
serial_nos.append(d.name)
return serial_nos
def validate_operation_data(row): def validate_operation_data(row):
if row.get("qty") <= 0: if row.get("qty") <= 0:
frappe.throw( frappe.throw(

View File

@@ -15,7 +15,6 @@ erpnext.patches.v10_0.rename_price_to_rate_in_pricing_rule
erpnext.patches.v10_0.set_currency_in_pricing_rule erpnext.patches.v10_0.set_currency_in_pricing_rule
erpnext.patches.v10_0.update_translatable_fields erpnext.patches.v10_0.update_translatable_fields
execute:frappe.delete_doc('DocType', 'Production Planning Tool', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Production Planning Tool', ignore_missing=True)
erpnext.patches.v10_0.add_default_cash_flow_mappers
erpnext.patches.v11_0.rename_duplicate_item_code_values erpnext.patches.v11_0.rename_duplicate_item_code_values
erpnext.patches.v11_0.make_quality_inspection_template erpnext.patches.v11_0.make_quality_inspection_template
erpnext.patches.v11_0.merge_land_unit_with_location erpnext.patches.v11_0.merge_land_unit_with_location
@@ -334,4 +333,9 @@ execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
erpnext.patches.v15_0.enable_all_leads erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.update_company_in_ldc
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template Details', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapper', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True)
erpnext.patches.v14_0.cleanup_workspaces erpnext.patches.v14_0.cleanup_workspaces

View File

@@ -1,15 +0,0 @@
# Copyright (c) 2017, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.setup.install import create_default_cash_flow_mapper_templates
def execute():
frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapping"))
frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapper"))
frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapping Template Details"))
create_default_cash_flow_mapper_templates()

View File

@@ -61,7 +61,6 @@ def execute():
doc.load_items_from_bom() doc.load_items_from_bom()
doc.calculate_rate_and_amount() doc.calculate_rate_and_amount()
set_expense_account(doc) set_expense_account(doc)
doc.make_batches("t_warehouse")
if doc.docstatus == 0: if doc.docstatus == 0:
doc.save() doc.save()

View File

@@ -25,20 +25,38 @@ frappe.listview_settings['Task'] = {
} }
return [__(doc.status), colors[doc.status], "status,=," + doc.status]; return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}, },
gantt_custom_popup_html: function(ganttobj, task) { gantt_custom_popup_html: function (ganttobj, task) {
var html = `<h5><a style="text-decoration:underline"\ let html = `
href="/app/task/${ganttobj.id}""> ${ganttobj.name} </a></h5>`; <a class="text-white mb-2 inline-block cursor-pointer"
href="/app/task/${ganttobj.id}"">
${ganttobj.name}
</a>
`;
if(task.project) html += `<p>Project: ${task.project}</p>`; if (task.project) {
html += `<p>Progress: ${ganttobj.progress}</p>`; html += `<p class="mb-1">${__("Project")}:
<a class="text-white inline-block"
href="/app/project/${task.project}"">
${task.project}
</a>
</p>`;
}
html += `<p class="mb-1">
${__("Progress")}:
<span class="text-white">${ganttobj.progress}%</span>
</p>`;
if(task._assign_list) { if (task._assign) {
html += task._assign_list.reduce( const assign_list = JSON.parse(task._assign);
(html, user) => html + frappe.avatar(user) const assignment_wrapper = `
, ''); <span>Assigned to:</span>
} <span class="text-white">
${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")}
return html; </span>
`;
html += assignment_wrapper;
} }
return `<div class="p-3" style="min-width: 220px">${html}</div>`;
},
}; };

View File

@@ -341,9 +341,79 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
} }
frappe.throw(msg); frappe.throw(msg);
} }
});
} }
);
}
}
add_serial_batch_bundle(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false;
frappe.require(path, function() {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let update_values = {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
}
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
}
);
});
}
});
}
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = true;
frappe.require(path, function() {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let update_values = {
"serial_and_batch_bundle": r.name,
"rejected_qty": Math.abs(r.total_qty)
}
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
}
);
});
}
});
} }
}; };

View File

@@ -6,6 +6,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
setup() { setup() {
super.setup(); super.setup();
let me = this; let me = this;
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frappe.flags.hide_serial_batch_dialog = true; frappe.flags.hide_serial_batch_dialog = true;
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) { frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
@@ -119,9 +122,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
}); });
if(this.frm.fields_dict["items"].grid.get_field('batch_no')) { if(this.frm.fields_dict["items"].grid.get_field('serial_and_batch_bundle')) {
this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) { this.frm.set_query("serial_and_batch_bundle", "items", function(doc, cdt, cdn) {
return me.set_query_for_batch(doc, cdt, cdn); let item_row = locals[cdt][cdn];
return {
filters: {
'item_code': item_row.item_code,
'voucher_type': doc.doctype,
'voucher_no': ["in", [doc.name, ""]],
}
}
}); });
} }
@@ -422,7 +432,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
update_stock = cint(me.frm.doc.update_stock); update_stock = cint(me.frm.doc.update_stock);
show_batch_dialog = update_stock; show_batch_dialog = update_stock;
} else if((this.frm.doc.doctype === 'Purchase Receipt' && me.frm.doc.is_return) || } else if((this.frm.doc.doctype === 'Purchase Receipt') ||
this.frm.doc.doctype === 'Delivery Note') { this.frm.doc.doctype === 'Delivery Note') {
show_batch_dialog = 1; show_batch_dialog = 1;
} }
@@ -514,6 +524,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (r.message && if (r.message &&
(r.message.has_batch_no || r.message.has_serial_no)) { (r.message.has_batch_no || r.message.has_serial_no)) {
frappe.flags.hide_serial_batch_dialog = false; frappe.flags.hide_serial_batch_dialog = false;
} else {
show_batch_dialog = false;
} }
}); });
}, },
@@ -528,7 +540,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}); });
}, },
() => { () => {
if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) { if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
$.each(r.message, function(k, v) { $.each(r.message, function(k, v) {
if(!d[k]) d[k] = v; if(!d[k]) d[k] = v;
@@ -538,12 +550,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
d.batch_no = undefined; d.batch_no = undefined;
} }
frappe.flags.dialog_set = true;
erpnext.show_serial_batch_selector(me.frm, d, (item) => { erpnext.show_serial_batch_selector(me.frm, d, (item) => {
me.frm.script_manager.trigger('qty', item.doctype, item.name); me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse) if (!me.frm.doc.set_warehouse)
me.frm.script_manager.trigger('warehouse', item.doctype, item.name); me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
me.apply_price_list(item, true); me.apply_price_list(item, true);
}, undefined, !frappe.flags.hide_serial_batch_dialog); }, undefined, !frappe.flags.hide_serial_batch_dialog);
} else {
frappe.flags.dialog_set = false;
} }
}, },
() => me.conversion_factor(doc, cdt, cdn, true), () => me.conversion_factor(doc, cdt, cdn, true),
@@ -672,6 +687,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
} }
on_submit() {
refresh_field("items");
}
update_qty(cdt, cdn) { update_qty(cdt, cdn) {
var valid_serial_nos = []; var valid_serial_nos = [];
var serialnos = []; var serialnos = [];
@@ -2272,12 +2291,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
}; };
erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_dialog) { erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) {
let warehouse, receiving_stock, existing_stock; let warehouse, receiving_stock, existing_stock;
let warehouse_field = "warehouse";
if (frm.doc.is_return) { if (frm.doc.is_return) {
if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) { if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) {
existing_stock = true; existing_stock = true;
warehouse = d.warehouse; warehouse = item_row.warehouse;
} else if (["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) { } else if (["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) {
receiving_stock = true; receiving_stock = true;
} }
@@ -2287,11 +2308,24 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
receiving_stock = true; receiving_stock = true;
} else { } else {
existing_stock = true; existing_stock = true;
warehouse = d.s_warehouse; warehouse = item_row.s_warehouse;
}
if (in_list([
"Material Transfer",
"Send to Subcontractor",
"Material Issue",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture"
], frm.doc.purpose)
) {
warehouse_field = "s_warehouse";
} else {
warehouse_field = "t_warehouse";
} }
} else { } else {
existing_stock = true; existing_stock = true;
warehouse = d.warehouse; warehouse = item_row.warehouse;
} }
} }
@@ -2304,16 +2338,29 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
} }
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
new erpnext.SerialNoBatchSelector({ if (in_list(["Sales Invoice", "Delivery Note"], frm.doc.doctype)) {
frm: frm, item_row.outward = frm.doc.is_return ? 0 : 1;
item: d, } else {
warehouse_details: { item_row.outward = frm.doc.is_return ? 1 : 0;
type: "Warehouse", }
name: warehouse
}, item_row.type_of_transaction = (item_row.outward === 1
callback: callback, ? "Outward":"Inward");
on_close: on_close
}, show_dialog); new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
let update_values = {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
}
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
}
});
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,260 +1,126 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash", "autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:27:51", "creation": "2013-02-22 01:27:51",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"item_code",
"serial_and_batch_bundle",
"serial_no",
"qty",
"description",
"prevdoc_detail_docname",
"prevdoc_docname",
"prevdoc_doctype"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code", "label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code", "oldfieldname": "item_code",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Item", "options": "Item",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Serial No", "label": "Serial No",
"length": 0, "no_copy": 1,
"no_copy": 0,
"oldfieldname": "serial_no", "oldfieldname": "serial_no",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text",
"permlevel": 0, "print_hide": 1,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "180px", "print_width": "180px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "180px" "width": "180px"
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Installed Qty", "label": "Installed Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "qty", "oldfieldname": "qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Description", "label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px", "print_width": "300px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "300px" "width": "300px"
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "prevdoc_detail_docname", "fieldname": "prevdoc_detail_docname",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Against Document Detail No", "label": "Against Document Detail No",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "prevdoc_detail_docname", "oldfieldname": "prevdoc_detail_docname",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px", "print_width": "150px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "150px" "width": "150px"
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "prevdoc_docname", "fieldname": "prevdoc_docname",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Against Document No", "label": "Against Document No",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "prevdoc_docname", "oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px", "print_width": "150px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1, "search_index": 1,
"set_only_once": 0,
"unique": 0,
"width": "150px" "width": "150px"
}, },
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "prevdoc_doctype", "fieldname": "prevdoc_doctype",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Document Type", "label": "Document Type",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "prevdoc_doctype", "oldfieldname": "prevdoc_doctype",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px", "print_width": "150px",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1, "search_index": 1,
"set_only_once": 0,
"unique": 0,
"width": "150px" "width": "150px"
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
} }
], ],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"menu_index": 0, "modified": "2023-03-12 13:47:08.257955",
"modified": "2017-02-20 13:24:18.142419",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Installation Note Item", "name": "Installation Note Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC", "sort_order": "ASC",
"track_changes": 1, "states": [],
"track_seen": 0 "track_changes": 1
} }

View File

@@ -415,9 +415,16 @@ class SalesOrder(SellingController):
def update_picking_status(self): def update_picking_status(self):
total_picked_qty = 0.0 total_picked_qty = 0.0
total_qty = 0.0 total_qty = 0.0
per_picked = 0.0
for so_item in self.items: for so_item in self.items:
if cint(
frappe.get_cached_value("Item", so_item.item_code, "is_stock_item")
) or self.has_product_bundle(so_item.item_code):
total_picked_qty += flt(so_item.picked_qty) total_picked_qty += flt(so_item.picked_qty)
total_qty += flt(so_item.stock_qty) total_qty += flt(so_item.stock_qty)
if total_picked_qty and total_qty:
per_picked = total_picked_qty / total_qty * 100 per_picked = total_picked_qty / total_qty * 100
self.db_set("per_picked", flt(per_picked), update_modified=False) self.db_set("per_picked", flt(per_picked), update_modified=False)

View File

@@ -1254,112 +1254,6 @@ class TestSalesOrder(FrappeTestCase):
) )
self.assertEqual(wo_qty[0][0], so_item_name.get(item)) self.assertEqual(wo_qty[0][0], so_item_name.get(item))
def test_serial_no_based_delivery(self):
frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1)
item = make_item(
"_Reserved_Serialized_Item",
{
"is_stock_item": 1,
"maintain_stock": 1,
"has_serial_no": 1,
"serial_no_series": "SI.####",
"valuation_rate": 500,
"item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}],
},
)
frappe.db.sql("""delete from `tabSerial No` where item_code=%s""", (item.item_code))
make_item(
"_Test Item A",
{
"maintain_stock": 1,
"valuation_rate": 100,
"item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}],
},
)
make_item(
"_Test Item B",
{
"maintain_stock": 1,
"valuation_rate": 200,
"item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}],
},
)
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Item A", "_Test Item B"])
so = make_sales_order(
**{
"item_list": [
{
"item_code": item.item_code,
"ensure_delivery_based_on_produced_serial_no": 1,
"qty": 1,
"rate": 1000,
}
]
}
)
so.submit()
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
work_order = make_wo_order_test_record(item=item.item_code, qty=1, do_not_save=True)
work_order.fg_warehouse = "_Test Warehouse - _TC"
work_order.sales_order = so.name
work_order.submit()
make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1)
item_serial_no = frappe.get_doc("Serial No", {"item_code": item.item_code})
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_production_stock_entry,
)
se = frappe.get_doc(make_production_stock_entry(work_order.name, "Manufacture", 1))
se.submit()
reserved_serial_no = se.get("items")[2].serial_no
serial_no_so = frappe.get_value("Serial No", reserved_serial_no, "sales_order")
self.assertEqual(serial_no_so, so.name)
dn = make_delivery_note(so.name)
dn.save()
self.assertEqual(reserved_serial_no, dn.get("items")[0].serial_no)
item_line = dn.get("items")[0]
item_line.serial_no = item_serial_no.name
item_line = dn.get("items")[0]
item_line.serial_no = reserved_serial_no
dn.submit()
dn.load_from_db()
dn.cancel()
si = make_sales_invoice(so.name)
si.update_stock = 1
si.save()
self.assertEqual(si.get("items")[0].serial_no, reserved_serial_no)
item_line = si.get("items")[0]
item_line.serial_no = item_serial_no.name
self.assertRaises(frappe.ValidationError, dn.submit)
item_line = si.get("items")[0]
item_line.serial_no = reserved_serial_no
self.assertTrue(si.submit)
si.submit()
si.load_from_db()
si.cancel()
si = make_sales_invoice(so.name)
si.update_stock = 0
si.submit()
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
make_delivery_note as make_delivery_note_from_invoice,
)
dn = make_delivery_note_from_invoice(si.name)
dn.save()
dn.submit()
self.assertEqual(dn.get("items")[0].serial_no, reserved_serial_no)
dn.load_from_db()
dn.cancel()
si.load_from_db()
si.cancel()
se.load_from_db()
se.cancel()
self.assertFalse(frappe.db.exists("Serial No", {"sales_order": so.name}))
def test_advance_payment_entry_unlink_against_sales_order(self): def test_advance_payment_entry_unlink_against_sales_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -1878,7 +1772,14 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(pe.references[1].reference_name, so.name) self.assertEqual(pe.references[1].reference_name, so.name)
self.assertEqual(pe.references[1].allocated_amount, 300) self.assertEqual(pe.references[1].allocated_amount, 300)
@change_settings("Stock Settings", {"enable_stock_reservation": 1}) @change_settings(
"Stock Settings",
{
"enable_stock_reservation": 1,
"auto_create_serial_and_batch_bundle_for_outward": 1,
"pick_serial_and_batch_based_on": "FIFO",
},
)
def test_stock_reservation_against_sales_order(self) -> None: def test_stock_reservation_against_sales_order(self) -> None:
from random import randint, uniform from random import randint, uniform

View File

@@ -44,7 +44,8 @@ erpnext.PointOfSale.ItemDetails = class {
<div class="item-image"></div> <div class="item-image"></div>
</div> </div>
<div class="discount-section"></div> <div class="discount-section"></div>
<div class="form-container"></div>` <div class="form-container"></div>
<div class="serial-batch-container"></div>`
) )
this.$item_name = this.$component.find('.item-name'); this.$item_name = this.$component.find('.item-name');
@@ -53,6 +54,7 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image = this.$component.find('.item-image'); this.$item_image = this.$component.find('.item-image');
this.$form_container = this.$component.find('.form-container'); this.$form_container = this.$component.find('.form-container');
this.$dicount_section = this.$component.find('.discount-section'); this.$dicount_section = this.$component.find('.discount-section');
this.$serial_batch_container = this.$component.find('.serial-batch-container');
} }
compare_with_current_item(item) { compare_with_current_item(item) {
@@ -101,12 +103,9 @@ erpnext.PointOfSale.ItemDetails = class {
const serialized = item_row.has_serial_no; const serialized = item_row.has_serial_no;
const batched = item_row.has_batch_no; const batched = item_row.has_batch_no;
const no_serial_selected = !item_row.serial_no; const no_bundle_selected = !item_row.serial_and_batch_bundle;
const no_batch_selected = !item_row.batch_no;
if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) {
frappe.show_alert({ frappe.show_alert({
message: __("Item is removed since no serial / batch no selected."), message: __("Item is removed since no serial / batch no selected."),
indicator: 'orange' indicator: 'orange'
@@ -200,13 +199,8 @@ erpnext.PointOfSale.ItemDetails = class {
} }
make_auto_serial_selection_btn(item) { make_auto_serial_selection_btn(item) {
if (item.has_serial_no) { if (item.has_serial_no || item.has_batch_no) {
if (!item.has_batch_no) { const label = item.has_serial_no ? __('Select Serial No') : __('Select Batch No');
this.$form_container.append(
`<div class="grid-filler no-select"></div>`
);
}
const label = __('Auto Fetch Serial Numbers');
this.$form_container.append( this.$form_container.append(
`<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>` `<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`
); );
@@ -382,40 +376,20 @@ erpnext.PointOfSale.ItemDetails = class {
bind_auto_serial_fetch_event() { bind_auto_serial_fetch_event() {
this.$form_container.on('click', '.auto-fetch-btn', () => { this.$form_container.on('click', '.auto-fetch-btn', () => {
this.batch_no_control && this.batch_no_control.set_value(''); frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => {
let qty = this.qty_control.get_value(); let frm = this.events.get_frm();
let conversion_factor = this.conversion_factor_control.get_value(); let item_row = this.item_row;
let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : ""; item_row.outward = 1;
item_row.type_of_transaction = "Outward";
let numbers = frappe.call({ new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number", if (r) {
args: { frappe.model.set_value(item_row.doctype, item_row.name, {
qty: qty * conversion_factor, "serial_and_batch_bundle": r.name,
item_code: this.current_item.item_code, "qty": Math.abs(r.total_qty)
warehouse: this.warehouse_control.get_value() || '', });
batch_nos: this.current_item.batch_no || '',
posting_date: expiry_date,
for_doctype: 'POS Invoice'
} }
}); });
numbers.then((data) => {
let auto_fetched_serial_numbers = data.message;
let records_length = auto_fetched_serial_numbers.length;
if (!records_length) {
const warehouse = this.warehouse_control.get_value().bold();
const item_code = this.current_item.item_code.bold();
frappe.msgprint(
__('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [item_code, warehouse])
);
} else if (records_length < qty) {
frappe.msgprint(
__('Fetched only {0} available serial numbers.', [records_length])
);
this.qty_control.set_value(records_length);
}
numbers = auto_fetched_serial_numbers.join(`\n`);
this.serial_no_control.set_value(numbers);
}); });
}) })
} }

View File

@@ -196,48 +196,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
refresh_field("incentives",row.name,row.parentfield); refresh_field("incentives",row.name,row.parentfield);
} }
warehouse(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
// check if serial nos entered are as much as qty in row
if (item.serial_no) {
let serial_nos = item.serial_no.split(`\n`).filter(sn => sn.trim()); // filter out whitespaces
if (item.qty === serial_nos.length) return;
}
if (item.serial_no && !item.batch_no) {
item.serial_no = null;
}
var has_batch_no;
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
has_batch_no = r && r.has_batch_no;
if(item.item_code && item.warehouse) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_bin_details_and_serial_nos",
child: item,
args: {
item_code: item.item_code,
warehouse: item.warehouse,
has_batch_no: has_batch_no || 0,
stock_qty: item.stock_qty,
serial_no: item.serial_no || "",
},
callback:function(r){
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
if (has_batch_no) {
me.set_batch_number(cdt, cdn);
me.batch_no(doc, cdt, cdn);
}
}
}
});
}
})
}
toggle_editable_price_list_rate() { toggle_editable_price_list_rate() {
var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name); var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name);
var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate")); var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
@@ -298,36 +256,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
} }
} }
batch_no(doc, cdt, cdn) {
super.batch_no(doc, cdt, cdn);
var item = frappe.get_doc(cdt, cdn);
if (item.serial_no) {
return;
}
item.serial_no = null;
var has_serial_no;
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_serial_no', (r) => {
has_serial_no = r && r.has_serial_no;
if(item.warehouse && item.item_code && item.batch_no) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_batch_qty_and_serial_no",
child: item,
args: {
"batch_no": item.batch_no,
"stock_qty": item.stock_qty || item.qty, //if stock_qty field is not available fetch qty (in case of Packed Items table)
"warehouse": item.warehouse,
"item_code": item.item_code,
"has_serial_no": has_serial_no
},
"fieldname": "actual_batch_qty"
});
}
})
}
set_dynamic_labels() { set_dynamic_labels() {
super.set_dynamic_labels(); super.set_dynamic_labels();
this.set_product_bundle_help(this.frm.doc); this.set_product_bundle_help(this.frm.doc);
@@ -372,50 +300,44 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) { conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) {
super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate); super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate);
if(frappe.meta.get_docfield(cdt, "stock_qty", cdn) &&
in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
this.set_batch_number(cdt, cdn);
}
} }
qty(doc, cdt, cdn) { qty(doc, cdt, cdn) {
super.qty(doc, cdt, cdn); super.qty(doc, cdt, cdn);
if(in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
this.set_batch_number(cdt, cdn);
}
} }
/* Determine appropriate batch number and set it in the form. pick_serial_and_batch(doc, cdt, cdn) {
* @param {string} cdt - Document Doctype let item = locals[cdt][cdn];
* @param {string} cdn - Document name let me = this;
*/ let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
set_batch_number(cdt, cdn) {
const doc = frappe.get_doc(cdt, cdn); frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
if (doc && doc.has_batch_no && doc.warehouse) { .then((r) => {
this._set_batch_number(doc); if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
} item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
item.outward = item.qty > 0 ? 1 : 0;
item.title = item.has_serial_no ?
__("Select Serial No") : __("Select Batch No");
if (item.has_serial_no && item.has_batch_no) {
item.title = __("Select Serial and Batch");
} }
_set_batch_number(doc) { frappe.require(path, function() {
if (doc.batch_no) { new erpnext.SerialBatchPackageSelector(
return me.frm, item, (r) => {
if (r) {
frappe.model.set_value(item.doctype, item.name, {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
});
} }
let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)};
if (doc.has_serial_no && doc.serial_no) {
args['serial_no'] = doc.serial_no
}
return frappe.call({
method: 'erpnext.stock.doctype.batch.batch.get_batch_no',
args: args,
callback: function(r) {
if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
} }
);
});
} }
}); });
} }

View File

@@ -8,7 +8,6 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.utils import cint from frappe.utils import cint
from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
@@ -23,7 +22,6 @@ def after_install():
set_single_defaults() set_single_defaults()
create_print_setting_custom_fields() create_print_setting_custom_fields()
add_all_roles_to("Administrator") add_all_roles_to("Administrator")
create_default_cash_flow_mapper_templates()
create_default_success_action() create_default_success_action()
create_default_energy_point_rules() create_default_energy_point_rules()
create_incoterms() create_incoterms()
@@ -116,13 +114,6 @@ def create_print_setting_custom_fields():
) )
def create_default_cash_flow_mapper_templates():
for mapper in DEFAULT_MAPPERS:
if not frappe.db.exists("Cash Flow Mapper", mapper["section_name"]):
doc = frappe.get_doc(mapper)
doc.insert(ignore_permissions=True)
def create_default_success_action(): def create_default_success_action():
for success_action in get_default_success_action(): for success_action in get_default_success_action():
if not frappe.db.exists("Success Action", success_action.get("ref_doctype")): if not frappe.db.exists("Success Action", success_action.get("ref_doctype")):

View File

@@ -36,7 +36,6 @@ def set_default_settings(args):
stock_settings.stock_uom = _("Nos") stock_settings.stock_uom = _("Nos")
stock_settings.auto_indent = 1 stock_settings.auto_indent = 1
stock_settings.auto_insert_price_list_rate_if_missing = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1
stock_settings.automatically_set_serial_nos_based_on_fifo = 1
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
stock_settings.save() stock_settings.save()

View File

@@ -486,7 +486,6 @@ def update_stock_settings():
stock_settings.stock_uom = _("Nos") stock_settings.stock_uom = _("Nos")
stock_settings.auto_indent = 1 stock_settings.auto_indent = 1
stock_settings.auto_insert_price_list_rate_if_missing = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1
stock_settings.automatically_set_serial_nos_based_on_fifo = 1
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
stock_settings.save() stock_settings.save()

View File

@@ -0,0 +1,237 @@
import frappe
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import flt
from frappe.utils.deprecations import deprecated
from pypika import Order
class DeprecatedSerialNoValuation:
@deprecated
def calculate_stock_value_from_deprecarated_ledgers(self):
serial_nos = list(
filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos())
)
actual_qty = flt(self.sle.actual_qty)
stock_value_change = 0
if actual_qty < 0:
if not self.sle.is_cancelled:
outgoing_value = self.get_incoming_value_for_serial_nos(serial_nos)
stock_value_change = -1 * outgoing_value
self.stock_value_change += stock_value_change
@deprecated
def get_incoming_value_for_serial_nos(self, serial_nos):
# get rate from serial nos within same company
all_serial_nos = frappe.get_all(
"Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
)
incoming_values = 0.0
for d in all_serial_nos:
if d.company == self.sle.company:
self.serial_no_incoming_rate[d.name] += flt(d.purchase_rate)
incoming_values += flt(d.purchase_rate)
# Get rate for serial nos which has been transferred to other company
invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company]
for serial_no in invalid_serial_nos:
table = frappe.qb.DocType("Stock Ledger Entry")
incoming_rate = (
frappe.qb.from_(table)
.select(table.incoming_rate)
.where(
(
(table.serial_no == serial_no)
| (table.serial_no.like(serial_no + "\n%"))
| (table.serial_no.like("%\n" + serial_no))
| (table.serial_no.like("%\n" + serial_no + "\n%"))
)
& (table.company == self.sle.company)
& (table.serial_and_batch_bundle.isnull())
& (table.actual_qty > 0)
& (table.is_cancelled == 0)
)
.orderby(table.posting_date, order=Order.desc)
.limit(1)
).run()
self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0
incoming_values += self.serial_no_incoming_rate[serial_no]
return incoming_values
class DeprecatedBatchNoValuation:
@deprecated
def calculate_avg_rate_from_deprecarated_ledgers(self):
entries = self.get_sle_for_batches()
for ledger in entries:
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
@deprecated
def get_sle_for_batches(self):
if not self.batchwise_valuation_batches:
return []
sle = frappe.qb.DocType("Stock Ledger Entry")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
self.sle.posting_date, self.sle.posting_time
)
if self.sle.creation:
timestamp_condition |= (
CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
) & (sle.creation < self.sle.creation)
query = (
frappe.qb.from_(sle)
.select(
sle.batch_no,
Sum(sle.stock_value_difference).as_("batch_value"),
Sum(sle.actual_qty).as_("batch_qty"),
)
.where(
(sle.item_code == self.sle.item_code)
& (sle.warehouse == self.sle.warehouse)
& (sle.batch_no.isin(self.batchwise_valuation_batches))
& (sle.batch_no.isnotnull())
& (sle.is_cancelled == 0)
)
.where(timestamp_condition)
.groupby(sle.batch_no)
)
if self.sle.name:
query = query.where(sle.name != self.sle.name)
return query.run(as_dict=True)
@deprecated
def calculate_avg_rate_for_non_batchwise_valuation(self):
if not self.non_batchwise_valuation_batches:
return
self.non_batchwise_balance_value = 0.0
self.non_batchwise_balance_qty = 0.0
self.set_balance_value_for_non_batchwise_valuation_batches()
for batch_no, ledger in self.batch_nos.items():
if batch_no not in self.non_batchwise_valuation_batches:
continue
self.batch_avg_rate[batch_no] = (
self.non_batchwise_balance_value / self.non_batchwise_balance_qty
)
stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
self.stock_value_change += stock_value_change
frappe.db.set_value(
"Serial and Batch Entry",
ledger.name,
{
"stock_value_difference": stock_value_change,
"incoming_rate": self.batch_avg_rate[batch_no],
},
)
@deprecated
def set_balance_value_for_non_batchwise_valuation_batches(self):
self.set_balance_value_from_sl_entries()
self.set_balance_value_from_bundle()
@deprecated
def set_balance_value_from_sl_entries(self) -> None:
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
self.sle.posting_date, self.sle.posting_time
)
if self.sle.creation:
timestamp_condition |= (
CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
) & (sle.creation < self.sle.creation)
query = (
frappe.qb.from_(sle)
.inner_join(batch)
.on(sle.batch_no == batch.name)
.select(
sle.batch_no,
Sum(sle.actual_qty).as_("batch_qty"),
Sum(sle.stock_value_difference).as_("batch_value"),
)
.where(
(sle.item_code == self.sle.item_code)
& (sle.warehouse == self.sle.warehouse)
& (sle.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (sle.is_cancelled == 0)
)
.where(timestamp_condition)
.groupby(sle.batch_no)
)
if self.sle.name:
query = query.where(sle.name != self.sle.name)
for d in query.run(as_dict=True):
self.non_batchwise_balance_value += flt(d.batch_value)
self.non_batchwise_balance_qty += flt(d.batch_qty)
self.available_qty[d.batch_no] += flt(d.batch_qty)
@deprecated
def set_balance_value_from_bundle(self) -> None:
bundle = frappe.qb.DocType("Serial and Batch Bundle")
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
batch = frappe.qb.DocType("Batch")
timestamp_condition = CombineDatetime(
bundle.posting_date, bundle.posting_time
) < CombineDatetime(self.sle.posting_date, self.sle.posting_time)
if self.sle.creation:
timestamp_condition |= (
CombineDatetime(bundle.posting_date, bundle.posting_time)
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
) & (bundle.creation < self.sle.creation)
query = (
frappe.qb.from_(bundle)
.inner_join(bundle_child)
.on(bundle.name == bundle_child.parent)
.inner_join(batch)
.on(bundle_child.batch_no == batch.name)
.select(
bundle_child.batch_no,
Sum(bundle_child.qty).as_("batch_qty"),
Sum(bundle_child.stock_value_difference).as_("batch_value"),
)
.where(
(bundle.item_code == self.sle.item_code)
& (bundle.warehouse == self.sle.warehouse)
& (bundle_child.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
& (bundle.type_of_transaction.isin(["Inward", "Outward"]))
)
.where(timestamp_condition)
.groupby(bundle_child.batch_no)
)
if self.sle.serial_and_batch_bundle:
query = query.where(bundle.name != self.sle.serial_and_batch_bundle)
for d in query.run(as_dict=True):
self.non_batchwise_balance_value += flt(d.batch_value)
self.non_batchwise_balance_qty += flt(d.batch_qty)
self.available_qty[d.batch_no] += flt(d.batch_qty)

View File

@@ -47,6 +47,8 @@ frappe.ui.form.on('Batch', {
return; return;
} }
debugger
const section = frm.dashboard.add_section('', __("Stock Levels")); const section = frm.dashboard.add_section('', __("Stock Levels"));
// sort by qty // sort by qty

View File

@@ -207,7 +207,7 @@
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"max_attachments": 5, "max_attachments": 5,
"modified": "2022-02-21 08:08:23.999236", "modified": "2023-03-12 15:56:09.516586",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Batch", "name": "Batch",

View File

@@ -2,12 +2,14 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CombineDatetime, CurDate, Sum from frappe.query_builder.functions import CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime from frappe.utils import cint, flt, get_link_to_form, nowtime, today
from frappe.utils.data import add_days from frappe.utils.data import add_days
from frappe.utils.jinja import render_template from frappe.utils.jinja import render_template
@@ -128,9 +130,7 @@ class Batch(Document):
frappe.throw(_("The selected item cannot have Batch")) frappe.throw(_("The selected item cannot have Batch"))
def set_batchwise_valuation(self): def set_batchwise_valuation(self):
from erpnext.stock.stock_ledger import get_valuation_method if self.is_new():
if self.is_new() and get_valuation_method(self.item) != "Moving Average":
self.use_batchwise_valuation = 1 self.use_batchwise_valuation = 1
def before_save(self): def before_save(self):
@@ -166,7 +166,12 @@ class Batch(Document):
@frappe.whitelist() @frappe.whitelist()
def get_batch_qty( def get_batch_qty(
batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None batch_no=None,
warehouse=None,
item_code=None,
posting_date=None,
posting_time=None,
ignore_voucher_nos=None,
): ):
"""Returns batch actual qty if warehouse is passed, """Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None or returns dict of qty by warehouse if warehouse is None
@@ -177,44 +182,31 @@ def get_batch_qty(
:param warehouse: Optional - give qty for this warehouse :param warehouse: Optional - give qty for this warehouse
:param item_code: Optional - give qty for this item""" :param item_code: Optional - give qty for this item"""
sle = frappe.qb.DocType("Stock Ledger Entry") from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
out = 0
if batch_no and warehouse:
query = (
frappe.qb.from_(sle)
.select(Sum(sle.actual_qty))
.where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no))
) )
if posting_date: batchwise_qty = defaultdict(float)
if posting_time is None: kwargs = frappe._dict(
posting_time = nowtime() {
"item_code": item_code,
query = query.where( "warehouse": warehouse,
CombineDatetime(sle.posting_date, sle.posting_time) "posting_date": posting_date,
<= CombineDatetime(posting_date, posting_time) "posting_time": posting_time,
"batch_no": batch_no,
"ignore_voucher_nos": ignore_voucher_nos,
}
) )
out = query.run(as_list=True)[0][0] or 0 batches = get_auto_batch_nos(kwargs)
if batch_no and not warehouse: if not (batch_no and warehouse):
out = ( return batches
frappe.qb.from_(sle)
.select(sle.warehouse, Sum(sle.actual_qty).as_("qty"))
.where((sle.is_cancelled == 0) & (sle.batch_no == batch_no))
.groupby(sle.warehouse)
).run(as_dict=True)
if not batch_no and item_code and warehouse: for batch in batches:
out = ( batchwise_qty[batch.get("batch_no")] += batch.get("qty")
frappe.qb.from_(sle)
.select(sle.batch_no, Sum(sle.actual_qty).as_("qty"))
.where((sle.is_cancelled == 0) & (sle.item_code == item_code) & (sle.warehouse == warehouse))
.groupby(sle.batch_no)
).run(as_dict=True)
return out return batchwise_qty[batch_no]
@frappe.whitelist() @frappe.whitelist()
@@ -230,13 +222,37 @@ def get_batches_by_oldest(item_code, warehouse):
@frappe.whitelist() @frappe.whitelist()
def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
"""Split the batch into a new batch""" """Split the batch into a new batch"""
batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert() batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert()
qty = flt(qty)
company = frappe.db.get_value( company = frappe.db.get_value("Warehouse", warehouse, "company")
"Stock Ledger Entry",
dict(item_code=item_code, batch_no=batch_no, warehouse=warehouse), from_bundle_id = make_batch_bundle(
["company"], frappe._dict(
{
"item_code": item_code,
"warehouse": warehouse,
"batches": frappe._dict({batch_no: qty}),
"company": company,
"type_of_transaction": "Outward",
"qty": qty,
}
)
)
to_bundle_id = make_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": warehouse,
"batches": frappe._dict({batch.name: qty}),
"company": company,
"type_of_transaction": "Inward",
"qty": qty,
}
)
) )
stock_entry = frappe.get_doc( stock_entry = frappe.get_doc(
@@ -245,8 +261,12 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
purpose="Repack", purpose="Repack",
company=company, company=company,
items=[ items=[
dict(item_code=item_code, qty=float(qty or 0), s_warehouse=warehouse, batch_no=batch_no), dict(
dict(item_code=item_code, qty=float(qty or 0), t_warehouse=warehouse, batch_no=batch.name), item_code=item_code, qty=qty, s_warehouse=warehouse, serial_and_batch_bundle=from_bundle_id
),
dict(
item_code=item_code, qty=qty, t_warehouse=warehouse, serial_and_batch_bundle=to_bundle_id
),
], ],
) )
) )
@@ -257,52 +277,27 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
return batch.name return batch.name
def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): def make_batch_bundle(kwargs):
"""Automatically select `batch_no` for outgoing items in item table""" from erpnext.stock.serial_batch_bundle import SerialBatchCreation
for d in doc.get(child_table):
qty = d.get("stock_qty") or d.get("transfer_qty") or d.get("qty") or 0 return (
warehouse = d.get(warehouse_field, None) SerialBatchCreation(
if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"): {
if not d.batch_no: "item_code": kwargs.item_code,
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) "warehouse": kwargs.warehouse,
else: "posting_date": today(),
batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse) "posting_time": nowtime(),
if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")): "voucher_type": "Stock Entry",
frappe.throw( "qty": flt(kwargs.qty),
_( "type_of_transaction": kwargs.type_of_transaction,
"Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches" "company": kwargs.company,
).format(d.idx, d.batch_no, batch_qty, qty) "batches": kwargs.batches,
"do_not_submit": True,
}
) )
.make_serial_and_batch_bundle()
.name
@frappe.whitelist()
def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
"""
Get batch number using First Expiring First Out method.
:param item_code: `item_code` of Item Document
:param warehouse: name of Warehouse to check
:param qty: quantity of Items
:return: String represent batch number of batch with sufficient quantity else an empty String
"""
batch_no = None
batches = get_batches(item_code, warehouse, qty, throw, serial_no)
for batch in batches:
if flt(qty) <= flt(batch.qty):
batch_no = batch.batch_id
break
if not batch_no:
frappe.msgprint(
_(
"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
).format(frappe.bold(item_code))
) )
if throw:
raise UnableToSelectBatchError
return batch_no
def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
@@ -362,10 +357,10 @@ def validate_serial_no_with_batch(serial_nos, item_code):
frappe.throw(_("There is no batch found against the {0}: {1}").format(message, serial_no_link)) frappe.throw(_("There is no batch found against the {0}: {1}").format(message, serial_no_link))
def make_batch(args): def make_batch(kwargs):
if frappe.db.get_value("Item", args.item, "has_batch_no"): if frappe.db.get_value("Item", kwargs.item, "has_batch_no"):
args.doctype = "Batch" kwargs.doctype = "Batch"
frappe.get_doc(args).insert().name return frappe.get_doc(kwargs).insert().name
@frappe.whitelist() @frappe.whitelist()
@@ -398,3 +393,28 @@ def get_pos_reserved_batch_qty(filters):
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
return flt_reserved_batch_qty return flt_reserved_batch_qty
def get_available_batches(kwargs):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
)
batchwise_qty = defaultdict(float)
batches = get_auto_batch_nos(kwargs)
for batch in batches:
batchwise_qty[batch.get("batch_no")] += batch.get("qty")
return batchwise_qty
def get_batch_no(bundle_id):
from erpnext.stock.serial_batch_bundle import get_batch_nos
batches = defaultdict(float)
for batch_id, d in get_batch_nos(bundle_id).items():
batches[batch_id] += abs(d.get("qty"))
return batches

View File

@@ -7,7 +7,7 @@ def get_data():
"transactions": [ "transactions": [
{"label": _("Buy"), "items": ["Purchase Invoice", "Purchase Receipt"]}, {"label": _("Buy"), "items": ["Purchase Invoice", "Purchase Receipt"]},
{"label": _("Sell"), "items": ["Sales Invoice", "Delivery Note"]}, {"label": _("Sell"), "items": ["Sales Invoice", "Delivery Note"]},
{"label": _("Move"), "items": ["Stock Entry"]}, {"label": _("Move"), "items": ["Serial and Batch Bundle"]},
{"label": _("Quality"), "items": ["Quality Inspection"]}, {"label": _("Quality"), "items": ["Quality Inspection"]},
], ],
} }

View File

@@ -10,15 +10,18 @@ from frappe.utils import cint, flt
from frappe.utils.data import add_to_date, getdate from frappe.utils.data import add_to_date, getdate
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( BatchNegativeStockError,
create_stock_reconciliation,
) )
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.serial_batch_bundle import SerialBatchCreation
class TestBatch(FrappeTestCase): class TestBatch(FrappeTestCase):
@@ -49,8 +52,10 @@ class TestBatch(FrappeTestCase):
).insert() ).insert()
receipt.submit() receipt.submit()
self.assertTrue(receipt.items[0].batch_no) receipt.load_from_db()
self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), batch_qty) self.assertTrue(receipt.items[0].serial_and_batch_bundle)
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
self.assertEqual(get_batch_qty(batch_no, receipt.items[0].warehouse), batch_qty)
return receipt return receipt
@@ -80,9 +85,12 @@ class TestBatch(FrappeTestCase):
stock_entry.insert() stock_entry.insert()
stock_entry.submit() stock_entry.submit()
self.assertTrue(stock_entry.items[0].batch_no) stock_entry.load_from_db()
bundle = stock_entry.items[0].serial_and_batch_bundle
self.assertTrue(bundle)
self.assertEqual( self.assertEqual(
get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90 get_batch_qty(get_batch_from_bundle(bundle), stock_entry.items[0].t_warehouse), 90
) )
def test_delivery_note(self): def test_delivery_note(self):
@@ -91,37 +99,71 @@ class TestBatch(FrappeTestCase):
receipt = self.test_purchase_receipt(batch_qty) receipt = self.test_purchase_receipt(batch_qty)
item_code = "ITEM-BATCH-1" item_code = "ITEM-BATCH-1"
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
bundle_id = (
SerialBatchCreation(
{
"item_code": item_code,
"warehouse": receipt.items[0].warehouse,
"actual_qty": batch_qty,
"voucher_type": "Stock Entry",
"batches": frappe._dict({batch_no: batch_qty}),
"type_of_transaction": "Outward",
"company": receipt.company,
}
)
.make_serial_and_batch_bundle()
.name
)
delivery_note = frappe.get_doc( delivery_note = frappe.get_doc(
dict( dict(
doctype="Delivery Note", doctype="Delivery Note",
customer="_Test Customer", customer="_Test Customer",
company=receipt.company, company=receipt.company,
items=[ items=[
dict(item_code=item_code, qty=batch_qty, rate=10, warehouse=receipt.items[0].warehouse) dict(
item_code=item_code,
qty=batch_qty,
rate=10,
warehouse=receipt.items[0].warehouse,
serial_and_batch_bundle=bundle_id,
)
], ],
) )
).insert() ).insert()
delivery_note.submit() delivery_note.submit()
receipt.load_from_db()
delivery_note.load_from_db()
# shipped from FEFO batch # shipped from FEFO batch
self.assertEqual( self.assertEqual(
delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) get_batch_from_bundle(delivery_note.items[0].serial_and_batch_bundle),
batch_no,
) )
def test_delivery_note_fail(self): def test_batch_negative_stock_error(self):
"""Test automatic batch selection for outgoing items""" """Test automatic batch selection for outgoing items"""
receipt = self.test_purchase_receipt(100) receipt = self.test_purchase_receipt(100)
delivery_note = frappe.get_doc(
dict( receipt.load_from_db()
doctype="Delivery Note", batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
customer="_Test Customer", sn_doc = SerialBatchCreation(
company=receipt.company, {
items=[ "item_code": "ITEM-BATCH-1",
dict(item_code="ITEM-BATCH-1", qty=5000, rate=10, warehouse=receipt.items[0].warehouse) "warehouse": receipt.items[0].warehouse,
], "voucher_type": "Delivery Note",
"qty": 5000,
"avg_rate": 10,
"batches": frappe._dict({batch_no: 5000}),
"type_of_transaction": "Outward",
"company": receipt.company,
}
) )
)
self.assertRaises(UnableToSelectBatchError, delivery_note.insert) self.assertRaises(BatchNegativeStockError, sn_doc.make_serial_and_batch_bundle)
def test_stock_entry_outgoing(self): def test_stock_entry_outgoing(self):
"""Test automatic batch selection for outgoing stock entry""" """Test automatic batch selection for outgoing stock entry"""
@@ -130,6 +172,24 @@ class TestBatch(FrappeTestCase):
receipt = self.test_purchase_receipt(batch_qty) receipt = self.test_purchase_receipt(batch_qty)
item_code = "ITEM-BATCH-1" item_code = "ITEM-BATCH-1"
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
bundle_id = (
SerialBatchCreation(
{
"item_code": item_code,
"warehouse": receipt.items[0].warehouse,
"actual_qty": batch_qty,
"voucher_type": "Stock Entry",
"batches": frappe._dict({batch_no: batch_qty}),
"type_of_transaction": "Outward",
"company": receipt.company,
}
)
.make_serial_and_batch_bundle()
.name
)
stock_entry = frappe.get_doc( stock_entry = frappe.get_doc(
dict( dict(
doctype="Stock Entry", doctype="Stock Entry",
@@ -140,6 +200,7 @@ class TestBatch(FrappeTestCase):
item_code=item_code, item_code=item_code,
qty=batch_qty, qty=batch_qty,
s_warehouse=receipt.items[0].warehouse, s_warehouse=receipt.items[0].warehouse,
serial_and_batch_bundle=bundle_id,
) )
], ],
) )
@@ -148,10 +209,11 @@ class TestBatch(FrappeTestCase):
stock_entry.set_stock_entry_type() stock_entry.set_stock_entry_type()
stock_entry.insert() stock_entry.insert()
stock_entry.submit() stock_entry.submit()
stock_entry.load_from_db()
# assert same batch is selected
self.assertEqual( self.assertEqual(
stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle),
get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle),
) )
def test_batch_split(self): def test_batch_split(self):
@@ -159,11 +221,11 @@ class TestBatch(FrappeTestCase):
receipt = self.test_purchase_receipt() receipt = self.test_purchase_receipt()
from erpnext.stock.doctype.batch.batch import split_batch from erpnext.stock.doctype.batch.batch import split_batch
new_batch = split_batch( batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22
)
self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 78) new_batch = split_batch(batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22)
self.assertEqual(get_batch_qty(batch_no, receipt.items[0].warehouse), 78)
self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22) self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22)
def test_get_batch_qty(self): def test_get_batch_qty(self):
@@ -174,7 +236,10 @@ class TestBatch(FrappeTestCase):
self.assertEqual( self.assertEqual(
get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"), get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"),
[{"batch_no": "batch a", "qty": 90.0}, {"batch_no": "batch b", "qty": 90.0}], [
{"batch_no": "batch a", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"},
{"batch_no": "batch b", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"},
],
) )
self.assertEqual(get_batch_qty("batch a", "_Test Warehouse - _TC"), 90) self.assertEqual(get_batch_qty("batch a", "_Test Warehouse - _TC"), 90)
@@ -201,6 +266,19 @@ class TestBatch(FrappeTestCase):
) )
batch.save() batch.save()
sn_doc = SerialBatchCreation(
{
"item_code": item_name,
"warehouse": warehouse,
"voucher_type": "Stock Entry",
"qty": 90,
"avg_rate": 10,
"batches": frappe._dict({batch_name: 90}),
"type_of_transaction": "Inward",
"company": "_Test Company",
}
).make_serial_and_batch_bundle()
stock_entry = frappe.get_doc( stock_entry = frappe.get_doc(
dict( dict(
doctype="Stock Entry", doctype="Stock Entry",
@@ -210,10 +288,10 @@ class TestBatch(FrappeTestCase):
dict( dict(
item_code=item_name, item_code=item_name,
qty=90, qty=90,
serial_and_batch_bundle=sn_doc.name,
t_warehouse=warehouse, t_warehouse=warehouse,
cost_center="Main - _TC", cost_center="Main - _TC",
rate=10, rate=10,
batch_no=batch_name,
allow_zero_valuation_rate=1, allow_zero_valuation_rate=1,
) )
], ],
@@ -320,7 +398,8 @@ class TestBatch(FrappeTestCase):
batches = {} batches = {}
for rate in rates: for rate in rates:
se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse) se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse)
batches[se.items[0].batch_no] = rate batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
batches[batch_no] = rate
LOW, HIGH = list(batches.keys()) LOW, HIGH = list(batches.keys())
@@ -341,7 +420,9 @@ class TestBatch(FrappeTestCase):
sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name}) sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name})
stock_value_difference = sle.actual_qty * batches[sle.batch_no] stock_value_difference = (
sle.actual_qty * batches[get_batch_from_bundle(sle.serial_and_batch_bundle)]
)
self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference) self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference)
stock_value += stock_value_difference stock_value += stock_value_difference
@@ -353,51 +434,12 @@ class TestBatch(FrappeTestCase):
self.assertEqual(json.loads(sle.stock_queue), []) # queues don't apply on batched items self.assertEqual(json.loads(sle.stock_queue), []) # queues don't apply on batched items
def test_moving_batch_valuation_rates(self):
item_code = "_TestBatchWiseVal"
warehouse = "_Test Warehouse - _TC"
self.make_batch_item(item_code)
def assertValuation(expected):
actual = get_valuation_rate(
item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no
)
self.assertAlmostEqual(actual, expected)
se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)
batch_no = se.items[0].batch_no
assertValuation(10)
# consumption should never affect current valuation rate
make_stock_entry(item_code=item_code, qty=20, source=warehouse)
assertValuation(10)
make_stock_entry(item_code=item_code, qty=30, source=warehouse)
assertValuation(10)
# 50 * 10 = 500 current value, add more item with higher valuation
make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no)
assertValuation(15)
# consuming again shouldn't do anything
make_stock_entry(item_code=item_code, qty=20, source=warehouse)
assertValuation(15)
# reset rate with stock reconiliation
create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no
)
assertValuation(25)
make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no)
assertValuation((20 * 20 + 10 * 25) / (10 + 20))
def test_update_batch_properties(self): def test_update_batch_properties(self):
item_code = "_TestBatchWiseVal" item_code = "_TestBatchWiseVal"
self.make_batch_item(item_code) self.make_batch_item(item_code)
se = make_stock_entry(item_code=item_code, qty=100, rate=10, target="_Test Warehouse - _TC") se = make_stock_entry(item_code=item_code, qty=100, rate=10, target="_Test Warehouse - _TC")
batch_no = se.items[0].batch_no batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
batch = frappe.get_doc("Batch", batch_no) batch = frappe.get_doc("Batch", batch_no)
expiry_date = add_to_date(batch.manufacturing_date, days=30) expiry_date = add_to_date(batch.manufacturing_date, days=30)
@@ -426,8 +468,17 @@ class TestBatch(FrappeTestCase):
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, batch_no=manually_created_batch) pr_1 = make_purchase_receipt(item_code=item_code, qty=1, batch_no=manually_created_batch)
pr_2 = make_purchase_receipt(item_code=item_code, qty=1) pr_2 = make_purchase_receipt(item_code=item_code, qty=1)
self.assertNotEqual(pr_1.items[0].batch_no, pr_2.items[0].batch_no) pr_1.load_from_db()
self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no) pr_2.load_from_db()
self.assertNotEqual(
get_batch_from_bundle(pr_1.items[0].serial_and_batch_bundle),
get_batch_from_bundle(pr_2.items[0].serial_and_batch_bundle),
)
self.assertEqual(
"BATCHEXISTING002", get_batch_from_bundle(pr_2.items[0].serial_and_batch_bundle)
)
def create_batch(item_code, rate, create_item_price_for_batch): def create_batch(item_code, rate, create_item_price_for_batch):

View File

@@ -12,7 +12,6 @@ from frappe.utils import cint, flt
from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
form_grid_templates = {"items": "templates/form_grid/item_grid.html"} form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -138,15 +137,11 @@ class DeliveryNote(SellingController):
self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc() self.validate_with_previous_doc()
self.set_serial_and_batch_bundle_from_pick_list()
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self) make_packing_list(self)
if self._action != "submit" and not self.is_return:
set_batch_nos(self, "warehouse", throw=True)
set_batch_nos(self, "warehouse", throw=True, child_table="packed_items")
self.update_current_stock() self.update_current_stock()
if not self.installation_status: if not self.installation_status:
@@ -193,6 +188,24 @@ class DeliveryNote(SellingController):
] ]
) )
def set_serial_and_batch_bundle_from_pick_list(self):
if not self.pick_list:
return
for item in self.items:
if item.pick_list_item:
filters = {
"item_code": item.item_code,
"voucher_type": "Pick List",
"voucher_no": self.pick_list,
"voucher_detail_no": item.pick_list_item,
}
bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name")
if bundle_id:
item.serial_and_batch_bundle = bundle_id
def validate_proj_cust(self): def validate_proj_cust(self):
"""check for does customer belong to same project as entered..""" """check for does customer belong to same project as entered.."""
if self.project and self.customer: if self.project and self.customer:
@@ -274,7 +287,12 @@ class DeliveryNote(SellingController):
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Serial and Batch Bundle",
)
def update_stock_reservation_entries(self) -> None: def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries.""" """Updates Delivered Qty in Stock Reservation Entries."""
@@ -1045,8 +1063,6 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"field_map": { "field_map": {
source_document_warehouse_field: target_document_warehouse_field, source_document_warehouse_field: target_document_warehouse_field,
"name": "delivery_note_item", "name": "delivery_note_item",
"batch_no": "batch_no",
"serial_no": "serial_no",
"purchase_order": "purchase_order", "purchase_order": "purchase_order",
"purchase_order_item": "purchase_order_item", "purchase_order_item": "purchase_order_item",
"material_request": "material_request", "material_request": "material_request",

View File

@@ -23,7 +23,11 @@ from erpnext.stock.doctype.delivery_note.delivery_note import (
) )
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError, get_serial_nos from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import ( from erpnext.stock.doctype.stock_entry.test_stock_entry import (
get_qty_after_transaction, get_qty_after_transaction,
make_serialized_item, make_serialized_item,
@@ -135,42 +139,6 @@ class TestDeliveryNote(FrappeTestCase):
dn.cancel() dn.cancel()
def test_serialized(self):
se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no)
self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name})
si = make_sales_invoice(dn.name)
si.insert(ignore_permissions=True)
self.assertEqual(dn.items[0].serial_no, si.items[0].serial_no)
dn.cancel()
self.check_serial_no_values(
serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""}
)
def test_serialized_partial_sales_invoice(self):
se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)
serial_no = "\n".join(serial_no)
dn = create_delivery_note(
item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no
)
si = make_sales_invoice(dn.name)
si.items[0].qty = 1
si.submit()
self.assertEqual(si.items[0].qty, 1)
si = make_sales_invoice(dn.name)
si.submit()
self.assertEqual(si.items[0].qty, len(get_serial_nos(si.items[0].serial_no)))
def test_serialize_status(self): def test_serialize_status(self):
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
@@ -178,16 +146,28 @@ class TestDeliveryNote(FrappeTestCase):
{ {
"doctype": "Serial No", "doctype": "Serial No",
"item_code": "_Test Serialized Item With Series", "item_code": "_Test Serialized Item With Series",
"serial_no": make_autoname("SR", "Serial No"), "serial_no": make_autoname("SRDD", "Serial No"),
} }
) )
serial_no.save() serial_no.save()
dn = create_delivery_note( bundle_id = make_serial_batch_bundle(
item_code="_Test Serialized Item With Series", serial_no=serial_no.name, do_not_submit=True frappe._dict(
{
"item_code": "_Test Serialized Item With Series",
"warehouse": "_Test Warehouse - _TC",
"qty": -1,
"voucher_type": "Delivery Note",
"serial_nos": [serial_no.name],
"posting_date": today(),
"posting_time": nowtime(),
"type_of_transaction": "Outward",
"do_not_save": True,
}
)
) )
self.assertRaises(SerialNoWarehouseError, dn.submit) self.assertRaises(frappe.ValidationError, bundle_id.make_serial_and_batch_bundle)
def check_serial_no_values(self, serial_no, field_values): def check_serial_no_values(self, serial_no, field_values):
serial_no = frappe.get_doc("Serial No", serial_no) serial_no = frappe.get_doc("Serial No", serial_no)
@@ -532,13 +512,14 @@ class TestDeliveryNote(FrappeTestCase):
def test_return_for_serialized_items(self): def test_return_for_serialized_items(self):
se = make_serialized_item() se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
serial_no = [get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]]
dn = create_delivery_note( dn = create_delivery_note(
item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no
) )
self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) self.check_serial_no_values(serial_no, {"warehouse": ""})
# return entry # return entry
dn1 = create_delivery_note( dn1 = create_delivery_note(
@@ -550,23 +531,17 @@ class TestDeliveryNote(FrappeTestCase):
serial_no=serial_no, serial_no=serial_no,
) )
self.check_serial_no_values( self.check_serial_no_values(serial_no, {"warehouse": "_Test Warehouse - _TC"})
serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""}
)
dn1.cancel() dn1.cancel()
self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) self.check_serial_no_values(serial_no, {"warehouse": ""})
dn.cancel() dn.cancel()
self.check_serial_no_values( self.check_serial_no_values(
serial_no, serial_no,
{ {"warehouse": "_Test Warehouse - _TC"},
"warehouse": "_Test Warehouse - _TC",
"delivery_document_no": "",
"purchase_document_no": se.name,
},
) )
def test_delivery_of_bundled_items_to_target_warehouse(self): def test_delivery_of_bundled_items_to_target_warehouse(self):
@@ -956,7 +931,7 @@ class TestDeliveryNote(FrappeTestCase):
"is_stock_item": 1, "is_stock_item": 1,
"has_batch_no": 1, "has_batch_no": 1,
"create_new_batch": 1, "create_new_batch": 1,
"batch_number_series": "TESTBATCH.#####", "batch_number_series": "TESTBATCHIUU.#####",
}, },
) )
make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) make_product_bundle(parent=batched_bundle.name, items=[batched_item.name])
@@ -964,16 +939,11 @@ class TestDeliveryNote(FrappeTestCase):
item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42 item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42
) )
try:
dn = create_delivery_note(item_code=batched_bundle.name, qty=1) dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
except frappe.ValidationError as e: dn.load_from_db()
if "batch" in str(e).lower():
self.fail("Batch numbers not getting added to bundled items in DN.")
raise e
self.assertTrue( batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle)
"TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item" self.assertTrue(batch_no)
)
def test_payment_terms_are_fetched_when_creating_sales_invoice(self): def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
@@ -1167,10 +1137,11 @@ class TestDeliveryNote(FrappeTestCase):
pi = make_purchase_receipt(qty=1, item_code=item.name) pi = make_purchase_receipt(qty=1, item_code=item.name)
dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no) pr_batch_no = get_batch_from_bundle(pi.items[0].serial_and_batch_bundle)
dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pr_batch_no)
dn.load_from_db() dn.load_from_db()
batch_no = dn.items[0].batch_no batch_no = get_batch_from_bundle(dn.items[0].serial_and_batch_bundle)
self.assertTrue(batch_no) self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
@@ -1241,6 +1212,36 @@ def create_delivery_note(**args):
dn.is_return = args.is_return dn.is_return = args.is_return
dn.return_against = args.return_against dn.return_against = args.return_against
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
type_of_transaction = args.type_of_transaction or "Outward"
if dn.is_return:
type_of_transaction = "Inward"
qty = args.get("qty") or 1
qty *= -1 if type_of_transaction == "Outward" else 1
batches = {}
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Delivery Note",
"serial_nos": args.serial_no,
"posting_date": dn.posting_date,
"posting_time": dn.posting_time,
"type_of_transaction": type_of_transaction,
"do_not_submit": True,
}
)
).name
dn.append( dn.append(
"items", "items",
{ {
@@ -1249,11 +1250,10 @@ def create_delivery_note(**args):
"qty": args.qty or 1, "qty": args.qty or 1,
"rate": args.rate if args.get("rate") is not None else 100, "rate": args.rate if args.get("rate") is not None else 100,
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_and_batch_bundle": bundle_id,
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1,
"expense_account": args.expense_account or "Cost of Goods Sold - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"batch_no": args.batch_no or None,
"target_warehouse": args.target_warehouse, "target_warehouse": args.target_warehouse,
}, },
) )
@@ -1262,6 +1262,9 @@ def create_delivery_note(**args):
dn.insert() dn.insert()
if not args.do_not_submit: if not args.do_not_submit:
dn.submit() dn.submit()
dn.load_from_db()
return dn return dn

View File

@@ -70,6 +70,7 @@
"target_warehouse", "target_warehouse",
"quality_inspection", "quality_inspection",
"col_break4", "col_break4",
"allow_zero_valuation_rate",
"against_sales_order", "against_sales_order",
"so_detail", "so_detail",
"against_sales_invoice", "against_sales_invoice",
@@ -77,8 +78,12 @@
"dn_detail", "dn_detail",
"pick_list_item", "pick_list_item",
"section_break_40", "section_break_40",
"batch_no", "pick_serial_and_batch",
"serial_and_batch_bundle",
"column_break_eaoe",
"serial_no", "serial_no",
"batch_no",
"available_qty_section",
"actual_batch_qty", "actual_batch_qty",
"actual_qty", "actual_qty",
"installed_qty", "installed_qty",
@@ -88,7 +93,6 @@
"received_qty", "received_qty",
"accounting_details_section", "accounting_details_section",
"expense_account", "expense_account",
"allow_zero_valuation_rate",
"column_break_71", "column_break_71",
"internal_transfer_section", "internal_transfer_section",
"material_request", "material_request",
@@ -505,17 +509,8 @@
}, },
{ {
"fieldname": "section_break_40", "fieldname": "section_break_40",
"fieldtype": "Section Break" "fieldtype": "Section Break",
}, "label": "Serial and Batch No"
{
"fieldname": "batch_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Batch No",
"oldfieldname": "batch_no",
"oldfieldtype": "Link",
"options": "Batch",
"print_hide": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -542,15 +537,6 @@
"read_only": 1, "read_only": 1,
"width": "150px" "width": "150px"
}, },
{
"fieldname": "serial_no",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
"oldfieldtype": "Text"
},
{ {
"fieldname": "item_group", "fieldname": "item_group",
"fieldtype": "Link", "fieldtype": "Link",
@@ -861,13 +847,51 @@
"no_copy": 1, "no_copy": 1,
"non_negative": 1, "non_negative": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
},
{
"collapsible": 1,
"fieldname": "available_qty_section",
"fieldtype": "Section Break",
"label": "Available Qty"
},
{
"fieldname": "column_break_eaoe",
"fieldtype": "Column Break"
},
{
"fieldname": "serial_no",
"fieldtype": "Text",
"hidden": 1,
"label": "Serial No",
"read_only": 1
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-05-01 21:05:14.175640", "modified": "2023-05-02 21:05:14.175640",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_to_date, flt, now from frappe.utils import add_days, add_to_date, flt, now, nowtime, today
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -15,6 +15,12 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries, get_gl_entries,
make_purchase_receipt, make_purchase_receipt,
) )
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.serial_batch_bundle import SerialNoValuation
class TestLandedCostVoucher(FrappeTestCase): class TestLandedCostVoucher(FrappeTestCase):
@@ -297,9 +303,8 @@ class TestLandedCostVoucher(FrappeTestCase):
self.assertEqual(expected_values[gle.account][1], gle.credit) self.assertEqual(expected_values[gle.account][1], gle.credit)
def test_landed_cost_voucher_for_serialized_item(self): def test_landed_cost_voucher_for_serialized_item(self):
frappe.db.sql( frappe.db.set_value("Item", "_Test Serialized Item", "serial_no_series", "SNJJ.###")
"delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')"
)
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
@@ -310,17 +315,42 @@ class TestLandedCostVoucher(FrappeTestCase):
) )
pr.items[0].item_code = "_Test Serialized Item" pr.items[0].item_code = "_Test Serialized Item"
pr.items[0].serial_no = "SN001\nSN002\nSN003\nSN004\nSN005"
pr.submit() pr.submit()
pr.load_from_db()
serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate") serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0]
sn_obj = SerialNoValuation(
sle=frappe._dict(
{
"posting_date": today(),
"posting_time": nowtime(),
"item_code": "_Test Serialized Item",
"warehouse": "Stores - TCP1",
"serial_nos": [serial_no],
}
)
)
serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no)
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) sn_obj = SerialNoValuation(
sle=frappe._dict(
{
"posting_date": today(),
"posting_time": nowtime(),
"item_code": "_Test Serialized Item",
"warehouse": "Stores - TCP1",
"serial_nos": [serial_no],
}
)
)
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
self.assertEqual(new_serial_no_rate - serial_no_rate, 5.0)
def test_serialized_lcv_delivered(self): def test_serialized_lcv_delivered(self):
"""In some cases you'd want to deliver before you can know all the """In some cases you'd want to deliver before you can know all the
@@ -337,23 +367,44 @@ class TestLandedCostVoucher(FrappeTestCase):
item_code = "_Test Serialized Item" item_code = "_Test Serialized Item"
warehouse = "Stores - TCP1" warehouse = "Stores - TCP1"
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"item_code": item_code,
"serial_no": serial_no,
}
).insert()
pr = make_purchase_receipt( pr = make_purchase_receipt(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse=warehouse, warehouse=warehouse,
qty=1, qty=1,
rate=200, rate=200,
item_code=item_code, item_code=item_code,
serial_no=serial_no, serial_no=[serial_no],
) )
serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") sn_obj = SerialNoValuation(
sle=frappe._dict(
{
"posting_date": today(),
"posting_time": nowtime(),
"item_code": "_Test Serialized Item",
"warehouse": "Stores - TCP1",
"serial_nos": [serial_no],
}
)
)
serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no)
# deliver it before creating LCV # deliver it before creating LCV
dn = create_delivery_note( dn = create_delivery_note(
item_code=item_code, item_code=item_code,
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
serial_no=serial_no, serial_no=[serial_no],
qty=1, qty=1,
rate=500, rate=500,
cost_center="Main - TCP1", cost_center="Main - TCP1",
@@ -362,14 +413,24 @@ class TestLandedCostVoucher(FrappeTestCase):
charges = 10 charges = 10
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + charges new_purchase_rate = serial_no_rate + charges
serial_no = frappe.db.get_value( sn_obj = SerialNoValuation(
"Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1 sle=frappe._dict(
{
"posting_date": today(),
"posting_time": nowtime(),
"item_code": "_Test Serialized Item",
"warehouse": "Stores - TCP1",
"serial_nos": [serial_no],
}
)
) )
self.assertEqual(serial_no.purchase_rate, new_purchase_rate) new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no)
# Since the serial no is already delivered the rate must be zero
self.assertFalse(new_serial_no_rate)
stock_value_difference = frappe.db.get_value( stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry", "Stock Ledger Entry",

View File

@@ -19,6 +19,8 @@
"rate", "rate",
"uom", "uom",
"section_break_9", "section_break_9",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"serial_no", "serial_no",
"column_break_11", "column_break_11",
"batch_no", "batch_no",
@@ -118,7 +120,8 @@
{ {
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Serial No" "label": "Serial No",
"read_only": 1
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
@@ -128,7 +131,8 @@
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
"options": "Batch" "options": "Batch",
"read_only": 1
}, },
{ {
"fieldname": "section_break_13", "fieldname": "section_break_13",
@@ -253,6 +257,19 @@
"no_copy": 1, "no_copy": 1,
"non_negative": 1, "non_negative": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
} }
], ],
"idx": 1, "idx": 1,

View File

@@ -3,6 +3,8 @@
frappe.ui.form.on('Pick List', { frappe.ui.form.on('Pick List', {
setup: (frm) => { setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
function(doc) { return (doc.stock_qty === 0) ? "red" : "green"; }); function(doc) { return (doc.stock_qty === 0) ? "red" : "green"; });

View File

@@ -12,14 +12,18 @@ from frappe.model.document import Document
from frappe.model.mapper import map_child_doc from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
from frappe.utils import cint, floor, flt, today from frappe.utils import cint, floor, flt
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import ( from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note as create_delivery_note_from_sales_order, make_delivery_note as create_delivery_note_from_sales_order,
) )
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
)
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
# TODO: Prioritize SO or WO group warehouse # TODO: Prioritize SO or WO group warehouse
@@ -59,38 +63,56 @@ class PickList(Document):
# if the user has not entered any picked qty, set it to stock_qty, before submit # if the user has not entered any picked qty, set it to stock_qty, before submit
item.picked_qty = item.stock_qty item.picked_qty = item.stock_qty
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
continue
if not item.serial_no:
frappe.throw(
_("Row #{0}: {1} does not have any available serial numbers in {2}").format(
frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)
),
title=_("Serial Nos Required"),
)
if len(item.serial_no.split("\n")) != item.picked_qty:
frappe.throw(
_(
"For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
).format(frappe.bold(item.item_code), frappe.bold(item.idx)),
title=_("Quantity Mismatch"),
)
def on_submit(self): def on_submit(self):
self.validate_serial_and_batch_bundle()
self.update_status() self.update_status()
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_reference_qty() self.update_reference_qty()
self.update_sales_order_picking_status() self.update_sales_order_picking_status()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = "Serial and Batch Bundle"
self.update_status() self.update_status()
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_reference_qty() self.update_reference_qty()
self.update_sales_order_picking_status() self.update_sales_order_picking_status()
self.delink_serial_and_batch_bundle()
def update_status(self, status=None): def delink_serial_and_batch_bundle(self):
for row in self.locations:
if row.serial_and_batch_bundle:
frappe.db.set_value(
"Serial and Batch Bundle",
row.serial_and_batch_bundle,
{"is_cancelled": 1, "voucher_no": ""},
)
row.db_set("serial_and_batch_bundle", None)
def on_update(self):
self.linked_serial_and_batch_bundle()
def linked_serial_and_batch_bundle(self):
for row in self.locations:
if row.serial_and_batch_bundle:
frappe.get_doc(
"Serial and Batch Bundle", row.serial_and_batch_bundle
).set_serial_and_batch_values(self, row)
def remove_serial_and_batch_bundle(self):
for row in self.locations:
if row.serial_and_batch_bundle:
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
def validate_serial_and_batch_bundle(self):
for row in self.locations:
if row.serial_and_batch_bundle:
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
if doc.docstatus == 0:
doc.submit()
def update_status(self, status=None, update_modified=True):
if not status: if not status:
if self.docstatus == 0: if self.docstatus == 0:
status = "Draft" status = "Draft"
@@ -192,6 +214,7 @@ class PickList(Document):
locations_replica = self.get("locations") locations_replica = self.get("locations")
# reset # reset
self.remove_serial_and_batch_bundle()
self.delete_key("locations") self.delete_key("locations")
updated_locations = frappe._dict() updated_locations = frappe._dict()
for item_doc in items: for item_doc in items:
@@ -265,6 +288,10 @@ class PickList(Document):
for item in locations: for item in locations:
if not item.item_code: if not item.item_code:
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
if not cint(
frappe.get_cached_value("Item", item.item_code, "is_stock_item")
) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
continue
item_code = item.item_code item_code = item.item_code
reference = item.sales_order_item or item.material_request_item reference = item.sales_order_item or item.material_request_item
key = (item_code, item.uom, item.warehouse, item.batch_no, reference) key = (item_code, item.uom, item.warehouse, item.batch_no, reference)
@@ -347,6 +374,7 @@ class PickList(Document):
pi_item.item_code, pi_item.item_code,
pi_item.warehouse, pi_item.warehouse,
pi_item.batch_no, pi_item.batch_no,
pi_item.serial_and_batch_bundle,
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_( Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
"picked_qty" "picked_qty"
), ),
@@ -476,18 +504,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
if not stock_qty: if not stock_qty:
break break
serial_nos = None
if item_location.serial_no:
serial_nos = "\n".join(item_location.serial_no[0 : cint(stock_qty)])
locations.append( locations.append(
frappe._dict( frappe._dict(
{ {
"qty": qty, "qty": qty,
"stock_qty": stock_qty, "stock_qty": stock_qty,
"warehouse": item_location.warehouse, "warehouse": item_location.warehouse,
"serial_no": serial_nos, "serial_and_batch_bundle": item_location.serial_and_batch_bundle,
"batch_no": item_location.batch_no,
} }
) )
) )
@@ -523,11 +546,7 @@ def get_available_item_locations(
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
if has_batch_no and has_serial_no: if has_serial_no:
locations = get_available_item_locations_for_serial_and_batched_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
)
elif has_serial_no:
locations = get_available_item_locations_for_serialized_item( locations = get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty item_code, from_warehouses, required_qty, company, total_picked_qty
) )
@@ -553,23 +572,6 @@ def get_available_item_locations(
if picked_item_details: if picked_item_details:
for location in list(locations): for location in list(locations):
key = (
(location["warehouse"], location["batch_no"])
if location.get("batch_no")
else location["warehouse"]
)
if key in picked_item_details:
picked_detail = picked_item_details[key]
if picked_detail.get("serial_no") and location.get("serial_no"):
location["serial_no"] = list(
set(location["serial_no"]).difference(set(picked_detail["serial_no"]))
)
location["qty"] = len(location["serial_no"])
else:
location["qty"] -= picked_detail.get("picked_qty")
if location["qty"] < 1: if location["qty"] < 1:
locations.remove(location) locations.remove(location)
@@ -595,7 +597,7 @@ def get_available_item_locations_for_serialized_item(
frappe.qb.from_(sn) frappe.qb.from_(sn)
.select(sn.name, sn.warehouse) .select(sn.name, sn.warehouse)
.where((sn.item_code == item_code) & (sn.company == company)) .where((sn.item_code == item_code) & (sn.company == company))
.orderby(sn.purchase_date) .orderby(sn.creation)
.limit(cint(required_qty + total_picked_qty)) .limit(cint(required_qty + total_picked_qty))
) )
@@ -607,12 +609,39 @@ def get_available_item_locations_for_serialized_item(
serial_nos = query.run(as_list=True) serial_nos = query.run(as_list=True)
warehouse_serial_nos_map = frappe._dict() warehouse_serial_nos_map = frappe._dict()
picked_qty = required_qty
for serial_no, warehouse in serial_nos: for serial_no, warehouse in serial_nos:
if picked_qty <= 0:
break
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no) warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
picked_qty -= 1
locations = [] locations = []
for warehouse, serial_nos in warehouse_serial_nos_map.items(): for warehouse, serial_nos in warehouse_serial_nos_map.items():
locations.append({"qty": len(serial_nos), "warehouse": warehouse, "serial_no": serial_nos}) qty = len(serial_nos)
bundle_doc = SerialBatchCreation(
{
"item_code": item_code,
"warehouse": warehouse,
"voucher_type": "Pick List",
"total_qty": qty * -1,
"serial_nos": serial_nos,
"type_of_transaction": "Outward",
"company": company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
locations.append(
{
"qty": qty,
"warehouse": warehouse,
"item_code": item_code,
"serial_and_batch_bundle": bundle_doc.name,
}
)
return locations return locations
@@ -620,64 +649,49 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item( def get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0 item_code, from_warehouses, required_qty, company, total_picked_qty=0
): ):
sle = frappe.qb.DocType("Stock Ledger Entry") locations = []
batch = frappe.qb.DocType("Batch") data = get_auto_batch_nos(
frappe._dict(
query = ( {
frappe.qb.from_(sle) "item_code": item_code,
.from_(batch) "warehouse": from_warehouses,
.select(sle.warehouse, sle.batch_no, Sum(sle.actual_qty).as_("qty")) "qty": required_qty + total_picked_qty,
.where( }
(sle.batch_no == batch.name)
& (sle.item_code == item_code)
& (sle.company == company)
& (batch.disabled == 0)
& (sle.is_cancelled == 0)
& (IfNull(batch.expiry_date, "2200-01-01") > today())
) )
.groupby(sle.warehouse, sle.batch_no, sle.item_code)
.having(Sum(sle.actual_qty) > 0)
.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
.limit(cint(required_qty + total_picked_qty))
) )
if from_warehouses: warehouse_wise_batches = frappe._dict()
query = query.where(sle.warehouse.isin(from_warehouses)) for d in data:
if d.warehouse not in warehouse_wise_batches:
warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float))
return query.run(as_dict=True) warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty
for warehouse, batches in warehouse_wise_batches.items():
qty = sum(batches.values())
def get_available_item_locations_for_serial_and_batched_item( bundle_doc = SerialBatchCreation(
item_code, from_warehouses, required_qty, company, total_picked_qty=0 {
): "item_code": item_code,
# Get batch nos by FIFO "warehouse": warehouse,
locations = get_available_item_locations_for_batched_item( "voucher_type": "Pick List",
item_code, from_warehouses, required_qty, company "total_qty": qty * -1,
"batches": batches,
"type_of_transaction": "Outward",
"company": company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
locations.append(
{
"qty": qty,
"warehouse": warehouse,
"item_code": item_code,
"serial_and_batch_bundle": bundle_doc.name,
}
) )
if locations:
sn = frappe.qb.DocType("Serial No")
conditions = (sn.item_code == item_code) & (sn.company == company)
for location in locations:
location.qty = (
required_qty if location.qty > required_qty else location.qty
) # if extra qty in batch
serial_nos = (
frappe.qb.from_(sn)
.select(sn.name)
.where(
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
)
.orderby(sn.purchase_date)
.limit(cint(location.qty + total_picked_qty))
).run(as_dict=True)
serial_nos = [sn.name for sn in serial_nos]
location.serial_no = serial_nos
location.qty = len(serial_nos)
return locations return locations

View File

@@ -11,6 +11,11 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError, EmptyStockReconciliationItemsError,
@@ -139,6 +144,18 @@ class TestPickList(FrappeTestCase):
self.assertEqual(pick_list.locations[1].qty, 10) self.assertEqual(pick_list.locations[1].qty, 10)
def test_pick_list_shows_serial_no_for_serialized_item(self): def test_pick_list_shows_serial_no_for_serialized_item(self):
serial_nos = ["SADD-0001", "SADD-0002", "SADD-0003", "SADD-0004", "SADD-0005"]
for serial_no in serial_nos:
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"company": "_Test Company",
"item_code": "_Test Serialized Item",
"serial_no": serial_no,
}
).insert()
stock_reconciliation = frappe.get_doc( stock_reconciliation = frappe.get_doc(
{ {
@@ -151,7 +168,20 @@ class TestPickList(FrappeTestCase):
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
"valuation_rate": 100, "valuation_rate": 100,
"qty": 5, "qty": 5,
"serial_no": "123450\n123451\n123452\n123453\n123454", "serial_and_batch_bundle": make_serial_batch_bundle(
frappe._dict(
{
"item_code": "_Test Serialized Item",
"warehouse": "_Test Warehouse - _TC",
"qty": 5,
"rate": 100,
"type_of_transaction": "Inward",
"do_not_submit": True,
"voucher_type": "Stock Reconciliation",
"serial_nos": serial_nos,
}
)
).name,
} }
], ],
} }
@@ -162,6 +192,10 @@ class TestPickList(FrappeTestCase):
except EmptyStockReconciliationItemsError: except EmptyStockReconciliationItemsError:
pass pass
so = make_sales_order(
item_code="_Test Serialized Item", warehouse="_Test Warehouse - _TC", qty=5, rate=1000
)
pick_list = frappe.get_doc( pick_list = frappe.get_doc(
{ {
"doctype": "Pick List", "doctype": "Pick List",
@@ -175,18 +209,20 @@ class TestPickList(FrappeTestCase):
"qty": 1000, "qty": 1000,
"stock_qty": 1000, "stock_qty": 1000,
"conversion_factor": 1, "conversion_factor": 1,
"sales_order": "_T-Sales Order-1", "sales_order": so.name,
"sales_order_item": "_T-Sales Order-1_item", "sales_order_item": so.items[0].name,
} }
], ],
} }
) )
pick_list.set_item_locations() pick_list.save()
self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item") self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item")
self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
self.assertEqual(pick_list.locations[0].qty, 5) self.assertEqual(pick_list.locations[0].qty, 5)
self.assertEqual(pick_list.locations[0].serial_no, "123450\n123451\n123452\n123453\n123454") self.assertEqual(
get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), serial_nos
)
def test_pick_list_shows_batch_no_for_batched_item(self): def test_pick_list_shows_batch_no_for_batched_item(self):
# check if oldest batch no is picked # check if oldest batch no is picked
@@ -245,8 +281,8 @@ class TestPickList(FrappeTestCase):
pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)
pr1.load_from_db() pr1.load_from_db()
oldest_batch_no = pr1.items[0].batch_no oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
oldest_serial_nos = pr1.items[0].serial_no oldest_serial_nos = get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle)
pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)
@@ -267,8 +303,12 @@ class TestPickList(FrappeTestCase):
) )
pick_list.set_item_locations() pick_list.set_item_locations()
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) self.assertEqual(
self.assertEqual(pick_list.locations[0].serial_no, oldest_serial_nos) get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no
)
self.assertEqual(
get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos
)
pr1.cancel() pr1.cancel()
pr2.cancel() pr2.cancel()
@@ -697,114 +737,3 @@ class TestPickList(FrappeTestCase):
pl.cancel() pl.cancel()
pl.reload() pl.reload()
self.assertEqual(pl.status, "Cancelled") self.assertEqual(pl.status, "Cancelled")
def test_consider_existing_pick_list(self):
def create_items(items_properties):
items = []
for properties in items_properties:
properties.update({"maintain_stock": 1})
item_code = make_item(properties=properties).name
properties.update({"item_code": item_code})
items.append(properties)
return items
def create_stock_entries(items):
warehouses = ["Stores - _TC", "Finished Goods - _TC"]
for item in items:
for warehouse in warehouses:
se = make_stock_entry(
item=item.get("item_code"),
to_warehouse=warehouse,
qty=5,
)
def get_item_list(items, qty, warehouse="All Warehouses - _TC"):
return [
{
"item_code": item.get("item_code"),
"qty": qty,
"warehouse": warehouse,
}
for item in items
]
def get_picked_items_details(pick_list_doc):
items_data = {}
for location in pick_list_doc.locations:
key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse
serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None
data = {"picked_qty": location.picked_qty}
if serial_no:
data["serial_no"] = serial_no
if location.item_code not in items_data:
items_data[location.item_code] = {key: data}
else:
items_data[location.item_code][key] = data
return items_data
# Step - 1: Setup - Create Items and Stock Entries
items_properties = [
{
"valuation_rate": 100,
},
{
"valuation_rate": 200,
"has_batch_no": 1,
"create_new_batch": 1,
},
{
"valuation_rate": 300,
"has_serial_no": 1,
"serial_no_series": "SNO.###",
},
{
"valuation_rate": 400,
"has_batch_no": 1,
"create_new_batch": 1,
"has_serial_no": 1,
"serial_no_series": "SNO.###",
},
]
items = create_items(items_properties)
create_stock_entries(items)
# Step - 2: Create Sales Order [1]
so1 = make_sales_order(item_list=get_item_list(items, qty=6))
# Step - 3: Create and Submit Pick List [1] for Sales Order [1]
pl1 = create_pick_list(so1.name)
pl1.submit()
# Step - 4: Create Sales Order [2] with same Item(s) as Sales Order [1]
so2 = make_sales_order(item_list=get_item_list(items, qty=4))
# Step - 5: Create Pick List [2] for Sales Order [2]
pl2 = create_pick_list(so2.name)
pl2.save()
# Step - 6: Assert
picked_items_details = get_picked_items_details(pl1)
for location in pl2.locations:
key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse
item_data = picked_items_details.get(location.item_code, {}).get(key, {})
picked_qty = item_data.get("picked_qty", 0)
picked_serial_no = picked_items_details.get("serial_no", [])
bin_actual_qty = frappe.db.get_value(
"Bin", {"item_code": location.item_code, "warehouse": location.warehouse}, "actual_qty"
)
# Available Qty to pick should be equal to [Actual Qty - Picked Qty]
self.assertEqual(location.stock_qty, bin_actual_qty - picked_qty)
# Serial No should not be in the Picked Serial No list
if location.serial_no:
a = set(picked_serial_no)
b = set([x for x in location.serial_no.split("\n") if x])
self.assertSetEqual(b, b.difference(a))

View File

@@ -21,6 +21,8 @@
"conversion_factor", "conversion_factor",
"stock_uom", "stock_uom",
"serial_no_and_batch_section", "serial_no_and_batch_section",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"serial_no", "serial_no",
"column_break_20", "column_break_20",
"batch_no", "batch_no",
@@ -72,14 +74,16 @@
"depends_on": "serial_no", "depends_on": "serial_no",
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Serial No" "label": "Serial No",
"read_only": 1
}, },
{ {
"depends_on": "batch_no", "depends_on": "batch_no",
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
"options": "Batch" "options": "Batch",
"read_only": 1
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
@@ -187,11 +191,24 @@
"hidden": 1, "hidden": 1,
"label": "Product Bundle Item", "label": "Product Bundle Item",
"read_only": 1 "read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-04-22 05:27:38.497997", "modified": "2023-03-12 13:50:22.258100",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List Item", "name": "Pick List Item",

View File

@@ -118,9 +118,7 @@ class PurchaseReceipt(BuyingController):
self.validate_posting_time() self.validate_posting_time()
super(PurchaseReceipt, self).validate() super(PurchaseReceipt, self).validate()
if self._action == "submit": if self._action != "submit":
self.make_batches("warehouse")
else:
self.set_status() self.set_status()
self.po_required() self.po_required()
@@ -242,11 +240,6 @@ class PurchaseReceipt(BuyingController):
# because updating ordered qty, reserved_qty_for_subcontract in bin # because updating ordered qty, reserved_qty_for_subcontract in bin
# depends upon updated ordered qty in PO # depends upon updated ordered qty in PO
self.update_stock_ledger() self.update_stock_ledger()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.set_consumed_qty_in_subcontract_order() self.set_consumed_qty_in_subcontract_order()
@@ -283,7 +276,12 @@ class PurchaseReceipt(BuyingController):
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Serial and Batch Bundle",
)
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order() self.set_consumed_qty_in_subcontract_order()

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today from frappe.utils import add_days, cint, cstr, flt, nowtime, today
from pypika import functions as fn from pypika import functions as fn
import erpnext import erpnext
@@ -11,7 +11,16 @@ from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
SerialNoDuplicateError,
SerialNoExistsInFutureTransactionError,
)
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
@@ -184,14 +193,11 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertTrue(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) self.assertTrue(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
pr.load_from_db() pr.load_from_db()
batch_no = pr.items[0].batch_no
pr.cancel() pr.cancel()
self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no}))
def test_duplicate_serial_nos(self): def test_duplicate_serial_nos(self):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"}) item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"})
if not item: if not item:
@@ -206,67 +212,86 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
pr.load_from_db() pr.load_from_db()
serial_nos = frappe.db.get_value( bundle_id = frappe.db.get_value(
"Stock Ledger Entry", "Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name}, {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name},
"serial_no", "serial_and_batch_bundle",
) )
serial_nos = get_serial_nos(serial_nos) serial_nos = get_serial_nos_from_bundle(bundle_id)
self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos) self.assertEquals(get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle), serial_nos)
# Then tried to receive same serial nos in difference company bundle_id = make_serial_batch_bundle(
pr_different_company = make_purchase_receipt( frappe._dict(
item_code=item.name, {
qty=2, "item_code": item.item_code,
rate=500, "warehouse": "_Test Warehouse 2 - _TC1",
serial_no="\n".join(serial_nos), "company": "_Test Company 1",
company="_Test Company 1", "qty": 2,
do_not_submit=True, "voucher_type": "Purchase Receipt",
warehouse="Stores - _TC1", "serial_nos": serial_nos,
"posting_date": today(),
"posting_time": nowtime(),
"do_not_save": True,
}
)
) )
self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) self.assertRaises(SerialNoDuplicateError, bundle_id.make_serial_and_batch_bundle)
# Then made delivery note to remove the serial nos from stock # Then made delivery note to remove the serial nos from stock
dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos)) dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no=serial_nos)
dn.load_from_db() dn.load_from_db()
self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) self.assertEquals(get_serial_nos_from_bundle(dn.items[0].serial_and_batch_bundle), serial_nos)
posting_date = add_days(today(), -3) posting_date = add_days(today(), -3)
# Try to receive same serial nos again in the same company with backdated. # Try to receive same serial nos again in the same company with backdated.
pr1 = make_purchase_receipt( bundle_id = make_serial_batch_bundle(
item_code=item.name, frappe._dict(
qty=2, {
rate=500, "item_code": item.item_code,
posting_date=posting_date, "warehouse": "_Test Warehouse - _TC",
serial_no="\n".join(serial_nos), "company": "_Test Company",
do_not_submit=True, "qty": 2,
"rate": 500,
"voucher_type": "Purchase Receipt",
"serial_nos": serial_nos,
"posting_date": posting_date,
"posting_time": nowtime(),
"do_not_save": True,
}
)
) )
self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) self.assertRaises(SerialNoExistsInFutureTransactionError, bundle_id.make_serial_and_batch_bundle)
# Try to receive same serial nos with different company with backdated. # Try to receive same serial nos with different company with backdated.
pr2 = make_purchase_receipt( bundle_id = make_serial_batch_bundle(
item_code=item.name, frappe._dict(
qty=2, {
rate=500, "item_code": item.item_code,
posting_date=posting_date, "warehouse": "_Test Warehouse 2 - _TC1",
serial_no="\n".join(serial_nos), "company": "_Test Company 1",
company="_Test Company 1", "qty": 2,
do_not_submit=True, "rate": 500,
warehouse="Stores - _TC1", "voucher_type": "Purchase Receipt",
"serial_nos": serial_nos,
"posting_date": posting_date,
"posting_time": nowtime(),
"do_not_save": True,
}
)
) )
self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) self.assertRaises(SerialNoExistsInFutureTransactionError, bundle_id.make_serial_and_batch_bundle)
# Receive the same serial nos after the delivery note posting date and time # Receive the same serial nos after the delivery note posting date and time
make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos)) make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no=serial_nos)
# Raise the error for backdated deliver note entry cancel # Raise the error for backdated deliver note entry cancel
self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) # self.assertRaises(SerialNoExistsInFutureTransactionError, dn.cancel)
def test_purchase_receipt_gl_entry(self): def test_purchase_receipt_gl_entry(self):
pr = make_purchase_receipt( pr = make_purchase_receipt(
@@ -307,11 +332,13 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.cancel() pr.cancel()
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
def test_serial_no_supplier(self): def test_serial_no_warehouse(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
pr_row_1_serial_no = pr.get("items")[0].serial_no pr_row_1_serial_no = get_serial_nos_from_bundle(pr.get("items")[0].serial_and_batch_bundle)[0]
self.assertEqual(frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), pr.supplier) self.assertEqual(
frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse"), pr.get("items")[0].warehouse
)
pr.cancel() pr.cancel()
self.assertFalse(frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse")) self.assertFalse(frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse"))
@@ -325,15 +352,18 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.get("items")[0].rejected_warehouse = "_Test Rejected Warehouse - _TC" pr.get("items")[0].rejected_warehouse = "_Test Rejected Warehouse - _TC"
pr.insert() pr.insert()
pr.submit() pr.submit()
pr.load_from_db()
accepted_serial_nos = pr.get("items")[0].serial_no.split("\n") accepted_serial_nos = get_serial_nos_from_bundle(pr.get("items")[0].serial_and_batch_bundle)
self.assertEqual(len(accepted_serial_nos), 3) self.assertEqual(len(accepted_serial_nos), 3)
for serial_no in accepted_serial_nos: for serial_no in accepted_serial_nos:
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].warehouse frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].warehouse
) )
rejected_serial_nos = pr.get("items")[0].rejected_serial_no.split("\n") rejected_serial_nos = get_serial_nos_from_bundle(
pr.get("items")[0].rejected_serial_and_batch_bundle
)
self.assertEqual(len(rejected_serial_nos), 2) self.assertEqual(len(rejected_serial_nos), 2)
for serial_no in rejected_serial_nos: for serial_no in rejected_serial_nos:
self.assertEqual( self.assertEqual(
@@ -556,23 +586,21 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0] serial_no = get_serial_nos_from_bundle(pr.get("items")[0].serial_and_batch_bundle)[0]
_check_serial_no_values( _check_serial_no_values(serial_no, {"warehouse": "_Test Warehouse - _TC"})
serial_no, {"warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name}
)
return_pr = make_purchase_receipt( return_pr = make_purchase_receipt(
item_code="_Test Serialized Item With Series", item_code="_Test Serialized Item With Series",
qty=-1, qty=-1,
is_return=1, is_return=1,
return_against=pr.name, return_against=pr.name,
serial_no=serial_no, serial_no=[serial_no],
) )
_check_serial_no_values( _check_serial_no_values(
serial_no, serial_no,
{"warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name}, {"warehouse": ""},
) )
return_pr.cancel() return_pr.cancel()
@@ -677,20 +705,23 @@ class TestPurchaseReceipt(FrappeTestCase):
item_code = "Test Manual Created Serial No" item_code = "Test Manual Created Serial No"
if not frappe.db.exists("Item", item_code): if not frappe.db.exists("Item", item_code):
item = make_item(item_code, dict(has_serial_no=1)) make_item(item_code, dict(has_serial_no=1))
serial_no = ["12903812901"]
if not frappe.db.exists("Serial No", serial_no[0]):
frappe.get_doc(
{"doctype": "Serial No", "item_code": item_code, "serial_no": serial_no[0]}
).insert()
serial_no = "12903812901"
pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
pr_doc.load_from_db()
self.assertEqual( bundle_id = pr_doc.items[0].serial_and_batch_bundle
serial_no, self.assertEqual(serial_no[0], get_serial_nos_from_bundle(bundle_id)[0])
frappe.db.get_value(
"Serial No",
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name},
"name",
),
)
voucher_no = frappe.db.get_value("Serial and Batch Bundle", bundle_id, "voucher_no")
self.assertEqual(voucher_no, pr_doc.name)
pr_doc.cancel() pr_doc.cancel()
# check for the auto created serial nos # check for the auto created serial nos
@@ -699,16 +730,15 @@ class TestPurchaseReceipt(FrappeTestCase):
make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###")) make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###"))
new_pr_doc = make_purchase_receipt(item_code=item_code, qty=1) new_pr_doc = make_purchase_receipt(item_code=item_code, qty=1)
new_pr_doc.load_from_db()
serial_no = get_serial_nos(new_pr_doc.items[0].serial_no)[0] bundle_id = new_pr_doc.items[0].serial_and_batch_bundle
self.assertEqual( serial_no = get_serial_nos_from_bundle(bundle_id)[0]
serial_no, self.assertTrue(serial_no)
frappe.db.get_value(
"Serial No", voucher_no = frappe.db.get_value("Serial and Batch Bundle", bundle_id, "voucher_no")
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name},
"name", self.assertEqual(voucher_no, new_pr_doc.name)
),
)
new_pr_doc.cancel() new_pr_doc.cancel()
@@ -1491,7 +1521,7 @@ class TestPurchaseReceipt(FrappeTestCase):
) )
pi.load_from_db() pi.load_from_db()
batch_no = pi.items[0].batch_no batch_no = get_batch_from_bundle(pi.items[0].serial_and_batch_bundle)
self.assertTrue(batch_no) self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
@@ -1917,6 +1947,30 @@ def make_purchase_receipt(**args):
item_code = args.item or args.item_code or "_Test Item" item_code = args.item or args.item_code or "_Test Item"
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
batches = {}
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
serial_nos = args.get("serial_no") or []
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Purchase Receipt",
"serial_nos": serial_nos,
"posting_date": args.posting_date or today(),
"posting_time": args.posting_time,
}
)
).name
pr.append( pr.append(
"items", "items",
{ {
@@ -1931,8 +1985,7 @@ def make_purchase_receipt(**args):
"rate": args.rate if args.rate != None else 50, "rate": args.rate if args.rate != None else 50,
"conversion_factor": args.conversion_factor or 1.0, "conversion_factor": args.conversion_factor or 1.0,
"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no, "serial_and_batch_bundle": bundle_id,
"batch_no": args.batch_no,
"stock_uom": args.stock_uom or "_Test UOM", "stock_uom": args.stock_uom or "_Test UOM",
"uom": uom, "uom": uom,
"cost_center": args.cost_center "cost_center": args.cost_center
@@ -1958,6 +2011,9 @@ def make_purchase_receipt(**args):
pr.insert() pr.insert()
if not args.do_not_submit: if not args.do_not_submit:
pr.submit() pr.submit()
pr.load_from_db()
return pr return pr

View File

@@ -79,6 +79,7 @@
"purchase_order", "purchase_order",
"purchase_invoice", "purchase_invoice",
"column_break_40", "column_break_40",
"allow_zero_valuation_rate",
"is_fixed_asset", "is_fixed_asset",
"asset_location", "asset_location",
"asset_category", "asset_category",
@@ -91,14 +92,19 @@
"delivery_note_item", "delivery_note_item",
"putaway_rule", "putaway_rule",
"section_break_45", "section_break_45",
"allow_zero_valuation_rate", "add_serial_batch_bundle",
"bom", "serial_and_batch_bundle",
"serial_no",
"col_break5", "col_break5",
"include_exploded_items", "add_serial_batch_for_rejected_qty",
"batch_no", "rejected_serial_and_batch_bundle",
"section_break_3vxt",
"serial_no",
"rejected_serial_no", "rejected_serial_no",
"item_tax_rate", "column_break_tolu",
"batch_no",
"subcontract_bom_section",
"include_exploded_items",
"bom",
"item_weight_details", "item_weight_details",
"weight_per_unit", "weight_per_unit",
"total_weight", "total_weight",
@@ -110,6 +116,7 @@
"manufacturer_part_no", "manufacturer_part_no",
"accounting_details_section", "accounting_details_section",
"expense_account", "expense_account",
"item_tax_rate",
"column_break_102", "column_break_102",
"provisional_expense_account", "provisional_expense_account",
"accounting_dimensions_section", "accounting_dimensions_section",
@@ -565,37 +572,8 @@
}, },
{ {
"fieldname": "section_break_45", "fieldname": "section_break_45",
"fieldtype": "Section Break" "fieldtype": "Section Break",
}, "label": "Serial and Batch No"
{
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
"oldfieldtype": "Text"
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "batch_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Batch No",
"no_copy": 1,
"oldfieldname": "batch_no",
"oldfieldtype": "Link",
"options": "Batch",
"print_hide": 1
},
{
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "rejected_serial_no",
"fieldtype": "Small Text",
"label": "Rejected Serial No",
"no_copy": 1,
"print_hide": 1
}, },
{ {
"fieldname": "item_tax_template", "fieldname": "item_tax_template",
@@ -1016,12 +994,70 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
},
{
"depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "subcontract_bom_section",
"fieldtype": "Section Break",
"label": "Subcontract BOM"
},
{
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No",
"read_only": 1
},
{
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
"label": "Rejected Serial No",
"read_only": 1
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
"read_only": 1
},
{
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle"
},
{
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
},
{
"fieldname": "section_break_3vxt",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_tolu",
"fieldtype": "Column Break"
},
{
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-02-28 15:43:04.470104", "modified": "2023-03-12 13:37:47.778021",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@@ -11,7 +11,6 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, cstr, floor, flt, nowdate from frappe.utils import cint, cstr, floor, flt, nowdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance from erpnext.stock.utils import get_stock_balance
@@ -99,7 +98,6 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
item = frappe._dict(item) item = frappe._dict(item)
source_warehouse = item.get("s_warehouse") source_warehouse = item.get("s_warehouse")
serial_nos = get_serial_nos(item.get("serial_no"))
item.conversion_factor = flt(item.conversion_factor) or 1.0 item.conversion_factor = flt(item.conversion_factor) or 1.0
pending_qty, item_code = flt(item.qty), item.item_code pending_qty, item_code = flt(item.qty), item.item_code
pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty)
@@ -145,9 +143,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if not qty_to_allocate: if not qty_to_allocate:
break break
updated_table = add_row( updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, rule.name)
item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos
)
pending_stock_qty -= stock_qty_to_allocate pending_stock_qty -= stock_qty_to_allocate
pending_qty -= qty_to_allocate pending_qty -= qty_to_allocate
@@ -245,7 +241,7 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
return False, vacant_rules return False, vacant_rules
def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): def add_row(item, to_allocate, warehouse, updated_table, rule=None):
new_updated_table_row = copy.deepcopy(item) new_updated_table_row = copy.deepcopy(item)
new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1
new_updated_table_row.name = None new_updated_table_row.name = None
@@ -264,8 +260,8 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N
if rule: if rule:
new_updated_table_row.putaway_rule = rule new_updated_table_row.putaway_rule = rule
if serial_nos:
new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate) new_updated_table_row.serial_and_batch_bundle = ""
updated_table.append(new_updated_table_row) updated_table.append(new_updated_table_row)
return updated_table return updated_table
@@ -297,12 +293,3 @@ def show_unassigned_items_message(items_not_accomodated):
) )
frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)
def get_serial_nos_to_allocate(serial_nos, to_allocate):
if serial_nos:
allocated_serial_nos = serial_nos[0 : cint(to_allocate)]
serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list
return "\n".join(allocated_serial_nos) if allocated_serial_nos else ""
else:
return ""

View File

@@ -7,6 +7,11 @@ from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
@@ -382,42 +387,49 @@ class TestPutawayRule(FrappeTestCase):
make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle")
pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1) pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1)
pr.items[0].batch_no = "BOTTL-BATCH-1"
pr.save() pr.save()
pr.submit() pr.submit()
pr.load_from_db()
serial_nos = frappe.get_list( batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
"Serial No", filters={"purchase_document_no": pr.name, "status": "Active"} serial_nos = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)
)
serial_nos = [d.name for d in serial_nos]
stock_entry = make_stock_entry( stock_entry = make_stock_entry(
item_code="Water Bottle", item_code="Water Bottle",
source="_Test Warehouse - _TC", source="_Test Warehouse - _TC",
qty=5, qty=5,
serial_no=serial_nos,
target="Finished Goods - _TC", target="Finished Goods - _TC",
purpose="Material Transfer", purpose="Material Transfer",
apply_putaway_rule=1, apply_putaway_rule=1,
do_not_save=1, do_not_save=1,
) )
stock_entry.items[0].batch_no = "BOTTL-BATCH-1"
stock_entry.items[0].serial_no = "\n".join(serial_nos)
stock_entry.save() stock_entry.save()
stock_entry.load_from_db()
self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
self.assertEqual(stock_entry.items[0].qty, 3) self.assertEqual(stock_entry.items[0].qty, 3)
self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name)
self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3])) self.assertEqual(
self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1") get_serial_nos_from_bundle(stock_entry.items[0].serial_and_batch_bundle), serial_nos[0:3]
)
self.assertEqual(get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle), batch_no)
self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2)
self.assertEqual(stock_entry.items[1].qty, 2) self.assertEqual(stock_entry.items[1].qty, 2)
self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name)
self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) self.assertEqual(
self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") get_serial_nos_from_bundle(stock_entry.items[1].serial_and_batch_bundle), serial_nos[3:5]
)
self.assertEqual(get_batch_from_bundle(stock_entry.items[1].serial_and_batch_bundle), batch_no)
self.assertUnchangedItemsOnResave(stock_entry) self.assertUnchangedItemsOnResave(stock_entry)
for row in stock_entry.items:
if row.serial_and_batch_bundle:
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
stock_entry.load_from_db()
stock_entry.delete() stock_entry.delete()
pr.cancel() pr.cancel()
rule_1.delete() rule_1.delete()

View File

@@ -0,0 +1,206 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Serial and Batch Bundle', {
setup(frm) {
frm.trigger('set_queries');
},
refresh(frm) {
frm.trigger('toggle_fields');
frm.trigger('prepare_serial_batch_prompt');
},
item_code(frm) {
frm.clear_custom_buttons();
frm.trigger('prepare_serial_batch_prompt');
},
type_of_transaction(frm) {
frm.clear_custom_buttons();
frm.trigger('prepare_serial_batch_prompt');
},
warehouse(frm) {
if (frm.doc.warehouse) {
frm.call({
method: "set_warehouse",
doc: frm.doc,
callback(r) {
refresh_field("entries");
}
})
}
},
has_serial_no(frm) {
frm.trigger('toggle_fields');
},
has_batch_no(frm) {
frm.trigger('toggle_fields');
},
prepare_serial_batch_prompt(frm) {
if (frm.doc.docstatus === 0 && frm.doc.item_code
&& frm.doc.type_of_transaction === "Inward") {
let label = frm.doc?.has_serial_no === 1
? __('Serial Nos') : __('Batch Nos');
if (frm.doc?.has_serial_no === 1 && frm.doc?.has_batch_no === 1) {
label = __('Serial and Batch Nos');
}
let fields = frm.events.get_prompt_fields(frm);
frm.add_custom_button(__("Make " + label), () => {
frappe.prompt(fields, (data) => {
frm.events.add_serial_batch(frm, data);
}, "Add " + label, "Make " + label);
});
}
},
get_prompt_fields(frm) {
let attach_field = {
"label": __("Attach CSV File"),
"fieldname": "csv_file",
"fieldtype": "Attach"
}
if (!frm.doc.has_batch_no) {
attach_field.depends_on = "eval:doc.using_csv_file === 1"
}
let fields = [
{
"label": __("Using CSV File"),
"fieldname": "using_csv_file",
"default": 1,
"fieldtype": "Check",
},
attach_field,
{
"fieldtype": "Section Break",
}
]
if (frm.doc.has_serial_no) {
fields.push({
"label": "Serial Nos",
"fieldname": "serial_nos",
"fieldtype": "Small Text",
"depends_on": "eval:doc.using_csv_file === 0"
})
}
if (frm.doc.has_batch_no) {
fields = attach_field
}
return fields;
},
add_serial_batch(frm, prompt_data) {
frm.events.validate_prompt_data(frm, prompt_data);
frm.call({
method: "add_serial_batch",
doc: frm.doc,
args: {
"data": prompt_data,
},
callback(r) {
refresh_field("entries");
}
});
},
validate_prompt_data(frm, prompt_data) {
if (prompt_data.using_csv_file && !prompt_data.csv_file) {
frappe.throw(__("Please attach CSV file"));
}
if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) {
frappe.throw(__("Please enter serial nos"));
}
},
toggle_fields(frm) {
frm.fields_dict.entries.grid.update_docfield_property(
'serial_no', 'read_only', !frm.doc.has_serial_no
);
frm.fields_dict.entries.grid.update_docfield_property(
'batch_no', 'read_only', !frm.doc.has_batch_no
);
},
set_queries(frm) {
frm.set_query('item_code', () => {
return {
query: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.item_query',
};
});
frm.set_query('voucher_type', () => {
return {
filters: {
'istable': 0,
'issingle': 0,
'is_submittable': 1,
}
};
});
frm.set_query('voucher_no', () => {
return {
filters: {
'docstatus': ["!=", 2],
}
};
});
frm.set_query('warehouse', () => {
return {
filters: {
'is_group': 0,
'company': frm.doc.company,
}
};
});
frm.set_query('serial_no', 'entries', () => {
return {
filters: {
item_code: frm.doc.item_code,
}
};
});
frm.set_query('batch_no', 'entries', () => {
return {
filters: {
item: frm.doc.item_code,
}
};
});
frm.set_query('warehouse', 'entries', () => {
return {
filters: {
company: frm.doc.company,
}
};
});
}
});
frappe.ui.form.on("Serial and Batch Entry", {
ledgers_add(frm, cdt, cdn) {
if (frm.doc.warehouse) {
locals[cdt][cdn].warehouse = frm.doc.warehouse;
}
},
})

Some files were not shown because too many files have changed in this diff Show More