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
commit 75387bbaef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,
}
)
cle.flags.ignore_permissions = True
cle.submit()

View File

@ -21,8 +21,6 @@
"allow_multi_currency_invoices_against_single_party_account",
"journals_section",
"merge_similar_account_heads",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section",
"book_deferred_entries_based_on",
"column_break_18",
@ -176,13 +174,6 @@
"fieldtype": "Int",
"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",
"description": "Payment Terms from orders will be fetched into the invoices as is",
@ -341,11 +332,6 @@
"fieldtype": "Tab Break",
"label": "POS"
},
{
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
},
{
"default": "0",
"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,
"issingle": 1,
"links": [],
"modified": "2023-05-17 12:20:04.107641",
"modified": "2023-06-01 15:42:44.912316",
"modified_by": "Administrator",
"module": "Accounts",
"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
from frappe import _
from frappe import _, bold
from frappe.query_builder.functions import IfNull, Sum
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,
)
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_delivered_serial_nos,
get_pos_reserved_serial_nos,
get_serial_nos,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class POSInvoice(SalesInvoice):
@ -71,6 +66,7 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True)
self.submit_serial_batch_bundle()
if self.coupon_code:
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")
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):
for pay in self.payments:
if pay.type == "Phone" and pay.amount >= 0:
@ -129,88 +148,6 @@ class POSInvoice(SalesInvoice):
if paid_amt and pay.amount != paid_amt:
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):
if self.is_return:
return
@ -223,13 +160,7 @@ class POSInvoice(SalesInvoice):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"):
if d.serial_no:
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 not d.serial_and_batch_bundle:
if is_negative_stock_allowed(item_code=d.item_code):
return
@ -258,36 +189,15 @@ class POSInvoice(SalesInvoice):
def validate_serialised_or_batched_item(self):
error_msg = []
for d in self.get("items"):
serialized = d.get("has_serial_no")
batched = d.get("has_batch_no")
no_serial_selected = not d.get("serial_no")
no_batch_selected = not d.get("batch_no")
error_msg = ""
if d.get("has_serial_no") and not d.serial_and_batch_bundle:
error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}"
msg = ""
item_code = frappe.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)
elif d.get("has_batch_no") and not d.serial_and_batch_bundle:
error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
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):
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)
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(
"Item", item.item_code, "is_stock_item"
):

View File

@ -5,12 +5,18 @@ import copy
import unittest
import frappe
from frappe import _
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.sales_invoice.test_sales_invoice import create_sales_invoice
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.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
@ -249,7 +255,7 @@ class TestPOSInvoice(unittest.TestCase):
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(
company="_Test Company",
@ -260,11 +266,11 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
serial_no=[serial_nos[0]],
rate=1000,
do_not_save=1,
)
pos.get("items")[0].serial_no = serial_nos[0]
pos.append(
"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.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):
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",
)
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(
company="_Test Company",
@ -300,12 +308,12 @@ class TestPOSInvoice(unittest.TestCase):
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
serial_no=serial_nos,
qty=2,
rate=1000,
do_not_save=1,
)
pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1]
pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
)
@ -317,14 +325,27 @@ class TestPOSInvoice(unittest.TestCase):
# partial return 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.submit()
# partial return 2
pos_return2 = make_sales_return(pos.name)
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):
pos = create_pos_invoice(
@ -368,7 +389,7 @@ class TestPOSInvoice(unittest.TestCase):
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(
company="_Test Company",
@ -380,10 +401,10 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
serial_no=[serial_nos[0]],
do_not_save=1,
)
pos.get("items")[0].serial_no = serial_nos[0]
pos.append(
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
)
@ -401,10 +422,10 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
serial_no=[serial_nos[0]],
do_not_save=1,
)
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append(
"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",
)
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(
company="_Test Company",
@ -435,11 +456,11 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
update_stock=1,
serial_no=[serial_nos[0]],
do_not_save=1,
)
si.get("items")[0].serial_no = serial_nos[0]
si.update_stock = 1
si.insert()
si.submit()
@ -453,10 +474,10 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
serial_no=[serial_nos[0]],
do_not_save=1,
)
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append(
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
)
@ -473,7 +494,7 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _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(
company="_Test Company",
@ -486,14 +507,13 @@ class TestPOSInvoice(unittest.TestCase):
item=se.get("items")[0].item_code,
rate=1000,
qty=2,
serial_nos=[serial_nos],
do_not_save=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):
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",
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
pos = create_pos_invoice(
@ -517,11 +537,11 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
serial_no=[serial_nos[0]],
qty=1,
do_not_save=1,
)
pos.get("items")[0].has_serial_no = 1
pos.get("items")[0].serial_no = serial_nos.split("\n")[0]
pos.set("payments", [])
pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
@ -547,12 +567,12 @@ class TestPOSInvoice(unittest.TestCase):
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
serial_no=[serial_nos[0]],
qty=1,
do_not_save=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
pos2.save()
@ -748,16 +768,16 @@ class TestPOSInvoice(unittest.TestCase):
self.assertEqual(rounded_total, 400)
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 (
create_batch_item_with_batch,
)
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01")
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(
target="_Test Warehouse - _TC",
@ -767,16 +787,28 @@ class TestPOSInvoice(unittest.TestCase):
batch_no="TestBatch 01",
)
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
pos_inv1.items[0].batch_no = "TestBatch 01"
pos_inv1 = create_pos_invoice(
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
)
pos_inv1.save()
pos_inv1.submit()
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
pos_inv1.reload()
@ -785,9 +817,6 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv2.reload()
pos_inv2.delete()
se.cancel()
batch.reload()
batch.cancel()
batch.delete()
def test_ignore_pricing_rule(self):
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")
try:
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.assertEquals(delivery_document_no, dn.name)
self.assertEqual(serial_no, delivered_serial_no)
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
serial_no=[serial_no],
qty=1,
rate=100,
do_not_submit=True,
@ -861,42 +890,6 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
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):
args = frappe._dict(args)
@ -926,6 +919,40 @@ def create_pos_invoice(**args):
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(
"items",
{
@ -936,8 +963,7 @@ def create_pos_invoice(**args):
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"batch_no": args.batch_no,
"serial_and_batch_bundle": bundle_id,
},
)

View File

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

View File

@ -184,6 +184,8 @@ class POSInvoiceMergeLog(Document):
item.base_amount = item.base_net_amount
item.price_list_rate = 0
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)
for tax in doc.get("taxes"):
@ -385,7 +387,7 @@ def split_invoices(invoices):
]
for pos_invoice in pos_return_docs:
for item in pos_invoice.items:
if not item.serial_no:
if not item.serial_no and not item.serial_and_batch_bundle:
continue
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 (
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
@ -410,13 +413,13 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
try:
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()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
serial_no=[serial_no],
qty=1,
rate=100,
do_not_submit=1,
@ -430,7 +433,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv2 = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=serial_no,
serial_no=[serial_no],
qty=1,
rate=100,
do_not_submit=1,

View File

@ -237,10 +237,6 @@ def apply_pricing_rule(args, doc=None):
item_list = args.get("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)
query_items = frappe.get_all(
"Item",
@ -258,28 +254,9 @@ def apply_pricing_rule(args, doc=None):
data = get_pricing_rule_for_item(args_copy, doc=doc)
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
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):
child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
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(self)
if self._action == "submit" and self.update_stock:
self.make_batches("warehouse")
self.validate_release_date()
self.check_conversion_rate()
self.validate_credit_to_acc()
@ -513,10 +510,6 @@ class PurchaseInvoice(BuyingController):
if self.is_old_subcontracting_flow:
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
self.make_gl_entries()
@ -1448,6 +1441,7 @@ class PurchaseInvoice(BuyingController):
"Repost Payment Ledger Items",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
"Serial and Batch Bundle",
)
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,
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.tests.test_utils import StockTestMixin
@ -888,14 +893,20 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
rejected_warehouse="_Test Rejected Warehouse - _TC",
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(
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,
)
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,
)
@ -1652,7 +1663,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
)
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)
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.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(
"items",
{
@ -1748,12 +1785,11 @@ def make_purchase_invoice(**args):
"discount_account": args.discount_account or None,
"discount_amount": args.discount_amount or 0,
"conversion_factor": 1.0,
"serial_no": args.serial_no,
"serial_and_batch_bundle": bundle_id,
"stock_uom": args.uom or "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project,
"rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or "",
"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:
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(
"items",
{
@ -1807,12 +1868,11 @@ def make_purchase_invoice_against_cost_center(**args):
"rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50,
"conversion_factor": 1.0,
"serial_no": args.serial_no,
"serial_and_batch_bundle": bundle_id,
"stock_uom": "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project,
"rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
},
)
if not args.do_not_save:

View File

@ -64,9 +64,11 @@
"warehouse",
"from_warehouse",
"quality_inspection",
"serial_and_batch_bundle",
"serial_no",
"col_br_wh",
"rejected_warehouse",
"rejected_serial_and_batch_bundle",
"batch_no",
"rejected_serial_no",
"manufacture_details",
@ -436,9 +438,10 @@
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"no_copy": 1,
"options": "Batch"
"options": "Batch",
"read_only": 1
},
{
"fieldname": "col_br_wh",
@ -448,8 +451,9 @@
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "serial_no",
"fieldtype": "Text",
"hidden": 1,
"label": "Serial No",
"no_copy": 1
"read_only": 1
},
{
"depends_on": "eval:!doc.is_fixed_asset",
@ -457,7 +461,8 @@
"fieldtype": "Text",
"label": "Rejected Serial No",
"no_copy": 1,
"print_hide": 1
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "accounting",
@ -875,12 +880,30 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"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,
"istable": 1,
"links": [],
"modified": "2022-11-29 13:01:20.438217",
"modified": "2023-04-01 20:08:54.545160",
"modified_by": "Administrator",
"module": "Accounts",
"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.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
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.serial_no.serial_no import (
get_delivery_note_serial_no,
get_serial_nos,
update_serial_nos_after_submit,
)
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@ -129,9 +124,6 @@ class SalesInvoice(SellingController):
if not self.is_opening:
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:
lp = frappe.get_doc("Loyalty Program", self.loyalty_program)
self.loyalty_redemption_account = (
@ -262,8 +254,6 @@ class SalesInvoice(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
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
self.make_gl_entries()
@ -276,8 +266,6 @@ class SalesInvoice(SellingController):
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit()
self.update_serial_no()
if not cint(self.is_pos) == 1 and not self.is_return:
self.update_against_document_in_jv()
@ -361,7 +349,6 @@ class SalesInvoice(SellingController):
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
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,
# 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 Items",
"Payment Ledger Entry",
"Serial and Batch Bundle",
)
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.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):
"""
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.item.test_item import create_item
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.stock_entry.test_stock_entry import (
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
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.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_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.submit()
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
def test_serialized_cancel(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
si = self.test_serialized()
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(
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):
"""
@ -1404,20 +1401,22 @@ class TestSalesInvoice(unittest.TestCase):
serial numbers are same
"""
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
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.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.save()
self.assertEqual(si.get("items")[0].serial_no, dn.get("items")[0].serial_no)
def test_return_sales_invoice(self):
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_time": si.posting_time,
"qty": -1 * flt(d.get("stock_qty")),
"serial_no": d.serial_no,
"serial_and_batch_bundle": d.serial_and_batch_bundle,
"company": si.company,
"voucher_type": "Sales Invoice",
"voucher_no": si.name,
@ -2982,7 +2981,7 @@ class TestSalesInvoice(unittest.TestCase):
# Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend(
si_with_payment_schedule.set(
"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
)
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):
try:
@ -3283,11 +3282,11 @@ class TestSalesInvoice(unittest.TestCase):
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.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)
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.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(
"items",
{
@ -3405,10 +3431,9 @@ def create_sales_invoice(**args):
"discount_amount": args.discount_amount or 0,
"asset": args.asset or None,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": args.get("conversion_factor", 1),
"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()
else:
si.payment_schedule = []
si.load_from_db()
else:
si.payment_schedule = []
@ -3452,7 +3479,6 @@ def create_sales_invoice_against_cost_center(**args):
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
},
)

View File

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

View File

@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.utils import cint, cstr
from frappe.utils import cstr
from erpnext.accounts.report.financial_statements import (
get_columns,
@ -20,11 +20,6 @@ from erpnext.accounts.utils import get_fiscal_year
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(
filters.from_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
from frappe import _
from frappe.utils import cint, flt, getdate
from frappe.utils import flt, getdate
import erpnext
from erpnext.accounts.report.balance_sheet.balance_sheet import (
@ -58,11 +58,6 @@ def execute(filters=None):
fiscal_year, companies, columns, filters
)
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)
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)
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`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`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}
{payment_term_cols}
from

View File

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

View File

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

View File

@ -65,6 +65,10 @@ class AssetCapitalization(StockController):
self.calculate_totals()
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):
self.validate_source_mandatory()
@ -74,7 +78,12 @@ class AssetCapitalization(StockController):
self.update_target_asset()
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.make_gl_entries()
self.update_target_asset()
@ -316,9 +325,7 @@ class AssetCapitalization(StockController):
for d in self.stock_items:
sle = self.get_sl_entries(
d,
{
"actual_qty": -flt(d.stock_qty),
},
{"actual_qty": -flt(d.stock_qty), "serial_and_batch_bundle": d.serial_and_batch_bundle},
)
sl_entries.append(sle)
@ -328,8 +335,6 @@ class AssetCapitalization(StockController):
{
"item_code": self.target_item_code,
"warehouse": self.target_warehouse,
"batch_no": self.target_batch_no,
"serial_no": self.target_serial_no,
"actual_qty": flt(self.target_qty),
"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,
)
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):
@ -371,14 +376,32 @@ def create_asset_capitalization(**args):
asset_capitalization.set_posting_time = 1
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(
"stock_items",
{
"item_code": args.stock_item or "Capitalization Source Stock Item",
"warehouse": source_warehouse,
"stock_qty": flt(args.stock_qty) or 1,
"batch_no": args.stock_batch_no,
"serial_no": args.stock_serial_no,
"serial_and_batch_bundle": bundle,
},
)

View File

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

View File

@ -147,6 +147,8 @@ class AssetRepair(AccountsController):
)
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
stock_entry.append(
"items",
{
@ -154,7 +156,7 @@ class AssetRepair(AccountsController):
"item_code": stock_item.item_code,
"qty": stock_item.consumed_quantity,
"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,
"project": self.project,
},
@ -165,6 +167,23 @@ class AssetRepair(AccountsController):
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):
if self.stock_entry:
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)

View File

@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import flt, nowdate
from frappe.utils import flt, nowdate, nowtime, today
from erpnext.assets.doctype.asset.asset import (
get_asset_account,
@ -19,6 +19,10 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_asset_depr_schedule_doc,
)
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):
@ -84,19 +88,19 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
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
stock_entry = make_serialized_item()
serial_nos = stock_entry.get("items")[0].serial_no
serial_no = serial_nos.split("\n")[0]
bundle_id = stock_entry.get("items")[0].serial_and_batch_bundle
serial_nos = get_serial_nos_from_bundle(bundle_id)
serial_no = serial_nos[0]
# should not raise any error
create_asset_repair(
stock_consumption=1,
item_code=stock_entry.get("items")[0].item_code,
warehouse="_Test Warehouse - _TC",
serial_no=serial_no,
serial_no=[serial_no],
submit=1,
)
@ -108,7 +112,7 @@ class TestAssetRepair(unittest.TestCase):
)
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):
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(
"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(
"stock_items",
{
"item_code": args.item_code or "_Test Stock Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100,
"consumed_quantity": args.qty or 1,
"serial_no": args.serial_no,
"serial_and_batch_bundle": bundle,
},
)

View File

@ -9,7 +9,8 @@
"valuation_rate",
"consumed_quantity",
"total_value",
"serial_no"
"serial_no",
"serial_and_batch_bundle"
],
"fields": [
{
@ -34,7 +35,9 @@
{
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No"
"hidden": 1,
"label": "Serial No",
"print_hide": 1
},
{
"fieldname": "item_code",
@ -42,12 +45,18 @@
"in_list_view": 1,
"label": "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,
"istable": 1,
"links": [],
"modified": "2022-02-08 17:37:20.028290",
"modified": "2023-04-06 02:24:20.375870",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
@ -55,5 +64,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"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()
dimension_dict = frappe._dict()
@ -2846,3 +2847,8 @@ def validate_regional(doc):
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass
@erpnext.allow_regional
def update_gl_dict_with_regional_fields(doc, gl_dict):
pass

View File

@ -5,7 +5,7 @@
import frappe
from frappe import ValidationError, _, msgprint
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 erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
@ -38,6 +38,7 @@ class BuyingController(SubcontractingController):
self.set_supplier_address()
self.validate_asset_return()
self.validate_auto_repeat_subscription_dates()
self.create_package_for_transfer()
if self.doctype == "Purchase Invoice":
self.validate_purchase_receipt_if_update_stock()
@ -58,6 +59,7 @@ class BuyingController(SubcontractingController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate()
self.set_serial_and_batch_bundle()
def onload(self):
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):
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_time": posting_time,
"qty": -1 * flt(d.get("stock_qty")),
"serial_no": d.get("serial_no"),
"batch_no": d.get("batch_no"),
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
@ -463,7 +494,15 @@ class BuyingController(SubcontractingController):
sl_entries.append(from_warehouse_sle)
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:
@ -471,7 +510,13 @@ class BuyingController(SubcontractingController):
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:
sle.dependant_sle_voucher_detail_no = d.name
else:
@ -504,20 +549,30 @@ class BuyingController(SubcontractingController):
{
"warehouse": d.rejected_warehouse,
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
"serial_no": cstr(d.rejected_serial_no).strip(),
"incoming_rate": 0.0,
"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
},
)
)
if self.get("is_old_subcontracting_flow"):
self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
allow_negative_stock=allow_negative_stock,
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):
po_map = {}
for d in self.get("items"):

View File

@ -3,12 +3,13 @@
import json
from collections import defaultdict
from collections import OrderedDict, defaultdict
import frappe
from frappe import scrub
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
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
def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
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)
searchfields = meta.get_search_fields()
search_columns = ""
search_cond = ""
query = get_batches_from_stock_ledger_entries(searchfields, txt, filters)
bundle_query = get_batches_from_serial_and_batch_bundle(searchfields, txt, filters)
if searchfields:
search_columns = ", " + ", ".join(searchfields)
search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
data = (
frappe.qb.from_((query) + (bundle_query))
.select("batch_no", "qty", "manufacturing_date", "expiry_date")
.offset(start)
.limit(page_len)
)
if args.get("warehouse"):
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])
for field in searchfields:
data = data.select(field)
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,
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:
batches[batch_data[0]][1] += batch_data[1]
filterd_batch = []
for batch, batch_data in batches.items():
if batch_data[1] > 0:
filterd_batch.append(tuple(batch_data))
return filterd_batch
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"),
)
return batch_nos
else:
return frappe.db.sql(
"""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
limit %(page_len)s offset %(start)s""".format(
cond,
search_columns=search_columns,
search_cond=search_cond,
match_conditions=get_match_cond(doctype),
),
args,
.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()

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):
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")
default_warehouse_for_sales_return = frappe.get_cached_value(
"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")
def update_item(source_doc, target_doc, source_parent):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
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 = 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)
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)
if source_doc.get("rejected_serial_no"):
returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field="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,
}
)
rejected_serial_nos = list(
set(get_serial_nos(source_doc.rejected_serial_no)) - set(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(
source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle"
)
type_of_transaction = "Inward"
if (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.rejected_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.rejected_serial_and_batch_bundle,
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
}
)
if rejected_serial_nos:
target_doc.rejected_serial_no = "\n".join(rejected_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"]:
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_time": sle.get("posting_time"),
"qty": sle.actual_qty,
"serial_no": sle.get("serial_no"),
"batch_no": sle.get("batch_no"),
"serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
"company": sle.company,
"voucher_type": sle.voucher_type,
"voucher_no": sle.voucher_no,
@ -620,8 +663,20 @@ def get_filters(
return filters
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def get_returned_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)
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 = []
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 = [
[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],
]
# 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):
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

View File

@ -5,7 +5,7 @@
import frappe
from frappe import _, bold, throw
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.sales_and_purchase_return import get_rate_for_return
@ -38,6 +38,9 @@ class SellingController(StockController):
self.validate_for_duplicate_items()
self.validate_target_warehouse()
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):
@ -299,8 +302,8 @@ class SellingController(StockController):
"item_code": p.item_code,
"qty": flt(p.qty),
"uom": p.uom,
"batch_no": cstr(p.batch_no).strip(),
"serial_no": cstr(p.serial_no).strip(),
"serial_and_batch_bundle": p.serial_and_batch_bundle
or get_serial_and_batch_bundle(p, self),
"name": d.name,
"target_warehouse": p.target_warehouse,
"company": self.company,
@ -323,8 +326,7 @@ class SellingController(StockController):
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
"batch_no": cstr(d.get("batch_no")).strip(),
"serial_no": cstr(d.get("serial_no")).strip(),
"serial_and_batch_bundle": d.serial_and_batch_bundle,
"name": d.name,
"target_warehouse": d.target_warehouse,
"company": self.company,
@ -337,6 +339,7 @@ class SellingController(StockController):
}
)
)
return il
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_time": self.get("posting_time") or nowtime(),
"qty": qty if cint(self.get("is_return")) else (-1 * qty),
"serial_no": d.get("serial_no"),
"batch_no": d.get("batch_no"),
"serial_and_batch_bundle": d.serial_and_batch_bundle,
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
@ -511,6 +513,7 @@ class SellingController(StockController):
"actual_qty": -1 * flt(item_row.qty),
"incoming_rate": item_row.incoming_rate,
"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):
@ -531,6 +534,11 @@ class SellingController(StockController):
if item_row.warehouse:
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
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 getattr(d, "income_account", None):
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
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
from erpnext.accounts.general_ledger import (
@ -325,29 +325,6 @@ class StockController(AccountsController):
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
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):
if not item.get("expense_account"):
msg = _("Please set an Expense Account in the Items table")
@ -387,27 +364,73 @@ class StockController(AccountsController):
)
def delete_auto_created_batches(self):
for d in self.items:
if not d.batch_no:
continue
for row in self.items:
if row.serial_and_batch_bundle:
frappe.db.set_value(
"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
)
frappe.db.set_value(
"Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None
)
row.db_set("serial_and_batch_bundle", None)
d.batch_no = None
d.db_set("batch_no", None)
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
if not table_name:
table_name = "items"
for data in frappe.get_all(
"Batch", {"reference_name": self.name, "reference_doctype": self.doctype}
):
frappe.delete_doc("Batch", data.name)
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
):
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):
sl_dict = frappe._dict(
{
"item_code": d.get("item_code", None),
"warehouse": d.get("warehouse", None),
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"fiscal_year": get_fiscal_year(self.posting_date, company=self.company)[0],
@ -420,8 +443,6 @@ class StockController(AccountsController):
),
"incoming_rate": 0,
"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"),
"is_cancelled": 1 if self.docstatus == 2 else 0,
}

View File

@ -8,10 +8,14 @@ from collections import defaultdict
import frappe
from frappe import _
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.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.serial_batch_bundle import SerialBatchCreation, get_serial_nos_from_bundle
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
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 = {
"item_code": "rm_item_code",
"subcontracted_item": "main_item_code",
@ -184,6 +192,7 @@ class SubcontractingController(StockController):
"basic_rate",
"amount",
"serial_no",
"serial_and_batch_bundle",
"uom",
"subcontracted_item",
"stock_uom",
@ -234,9 +243,11 @@ class SubcontractingController(StockController):
"serial_no",
"rm_item_code",
"reference_name",
"serial_and_batch_bundle",
"batch_no",
"consumed_qty",
"main_item_code",
"parent as voucher_no",
],
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())
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:
return (consumed_materials, receipt_items)
@ -262,11 +280,29 @@ class SubcontractingController(StockController):
continue
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:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
# Will be deprecated in v16
if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
@ -281,7 +317,16 @@ class SubcontractingController(StockController):
if not self.subcontract_orders:
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))
if key not in self.available_materials:
@ -310,6 +355,20 @@ class SubcontractingController(StockController):
if row.batch_no:
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.__transferred_items = copy.deepcopy(self.available_materials)
@ -327,6 +386,7 @@ class SubcontractingController(StockController):
self.set(self.raw_material_table, [])
for item in self._doc_before_save.supplied_items:
if item.reference_name in self.__changed_name:
self.__remove_serial_and_batch_bundle(item)
continue
if item.reference_name not in self.__reference_name:
@ -337,6 +397,10 @@ class SubcontractingController(StockController):
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):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
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):
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))
if not self.available_materials.get(key):
return
if (
not self.available_materials[key]["serial_no"] and not self.available_materials[key]["batch_no"]
):
return
serial_nos = []
batches = frappe._dict({})
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
rm_obj.serial_no = "\n".join(used_serial_nos)
serial_nos = self.__get_serial_nos_for_bundle(qty, key)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]["serial_no"].remove(sn)
elif self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
batches = self.__get_batch_nos_for_bundle(qty, key)
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),
}
)
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()
self.__set_serial_nos(item_row, rm_obj)
return bundle.name
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 __get_batch_nos_for_bundle(self, qty, key):
available_batches = defaultdict(float)
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))
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:
qty_to_consumed = batch_qty
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)
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
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 available_batches
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
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
def __get_serial_nos_for_bundle(self, qty, key):
available_sns = sorted(self.available_materials[key]["serial_no"])[0 : cint(qty)]
serial_nos = []
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
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):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
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":
args = frappe._dict(
{
@ -447,25 +532,23 @@ class SubcontractingController(StockController):
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty),
"serial_no": rm_obj.serial_no,
"batch_no": rm_obj.batch_no,
"actual_qty": -1 * flt(rm_obj.consumed_qty),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": item_row.name,
"company": self.company,
"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.required_qty = 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)
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
item_row, rm_obj, rm_obj.consumed_qty
)
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):
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.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):
self.initialized_fields()
self.__get_subcontract_orders()
@ -527,6 +657,7 @@ class SubcontractingController(StockController):
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
self.__modify_serial_and_batch_bundle()
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(
@ -539,8 +670,8 @@ class SubcontractingController(StockController):
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get("serial_no"):
serial_nos = get_serial_nos(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_from_bundle(row.get("serial_and_batch_bundle"))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
if incorrect_sn:
@ -667,9 +798,7 @@ class SubcontractingController(StockController):
scr_qty = flt(item.qty) * flt(item.conversion_factor)
if scr_qty:
sle = self.get_sl_entries(
item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()}
)
sle = self.get_sl_entries(item, {"actual_qty": flt(scr_qty)})
rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9
incoming_rate = flt(item.rate, rate_db_precision)
sle.update(
@ -687,7 +816,6 @@ class SubcontractingController(StockController):
{
"warehouse": item.rejected_warehouse,
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
"serial_no": cstr(item.rejected_serial_no).strip(),
"incoming_rate": 0.0,
},
)
@ -716,8 +844,7 @@ class SubcontractingController(StockController):
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * item.consumed_qty,
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"serial_and_batch_bundle": item.serial_and_batch_bundle,
}
)
@ -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:
rm_item_code = rm_item.get("rm_item_code")
items_dict = {
rm_item_code: {
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"),
"to_warehouse": subcontract_order.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_no": rm_item.get("serial_no"),
"batch_no": rm_item.get("batch_no"),
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"main_item_code": fg_item_code,
"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)
ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
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.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.stock_entry.test_stock_entry import make_stock_entry
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
@ -311,9 +316,6 @@ class TestSubcontractingController(FrappeTestCase):
scr1 = make_subcontracting_receipt(sco.name)
scr1.save()
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()
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.
- 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")
service_items = [
@ -426,6 +429,7 @@ class TestSubcontractingController(FrappeTestCase):
for key, value in get_supplied_items(scr1).items():
self.assertEqual(value.qty, 4)
frappe.flags.add_debugger = True
scr2 = make_subcontracting_receipt(sco.name)
scr2.items[0].qty = 2
add_second_row_in_scr(scr2)
@ -612,9 +616,6 @@ class TestSubcontractingController(FrappeTestCase):
scr1.load_from_db()
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.submit()
@ -651,6 +652,16 @@ class TestSubcontractingController(FrappeTestCase):
- 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")
service_items = [
{
@ -677,10 +688,39 @@ class TestSubcontractingController(FrappeTestCase):
scr1 = make_subcontracting_receipt(sco.name)
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)
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()
@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):
"""
- Set backflush based on Material Transferred for Subcontract.
@ -724,12 +764,9 @@ class TestSubcontractingController(FrappeTestCase):
for key, value in get_supplied_items(scr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, 3)
transferred_batch_no = details.batch_no
self.assertEqual(value.batch_no, details.batch_no)
scr1.load_from_db()
scr1.supplied_items[0].consumed_qty = 5
scr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
scr1.save()
scr1.submit()
@ -883,6 +920,15 @@ def update_item_details(child_row, details):
if child_row.batch_no:
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):
args = frappe._dict(args)
@ -903,18 +949,35 @@ def make_stock_transfer_entry(**args):
item_details = args.itemwise_details.get(row.item_code)
serial_nos = []
batches = defaultdict(float)
if item_details and item_details.serial_no:
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))
if item_details and item_details.batch_no:
for batch_no, batch_qty in item_details.batch_no.items():
if batch_qty >= row.qty:
item["batch_no"] = batch_no
batches[batch_no] = row.qty
item_details.batch_no[batch_no] -= row.qty
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)
ste_dict = make_rm_stock_entry(args.sco_no, items)
@ -956,7 +1019,7 @@ def make_raw_materials():
"batch_number_series": "BAT.####",
},
"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():

View File

@ -67,6 +67,12 @@ treeviews = [
"Department",
]
jinja = {
"methods": [
"erpnext.stock.serial_batch_bundle.get_serial_or_batch_nos",
],
}
# website
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('customer_address', erpnext.queries.address_query);
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) {
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.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
@ -74,10 +73,14 @@ class MaintenanceSchedule(TransactionBase):
email_map = {}
for d in self.get("items"):
if d.serial_no:
serial_nos = get_valid_serial_nos(d.serial_no)
self.validate_serial_no(d.item_code, serial_nos, d.start_date)
self.update_amc_date(serial_nos, d.end_date)
if d.serial_and_batch_bundle:
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.update_amc_date(serial_nos, d.end_date)
no_email_sp = []
if d.sales_person not in email_map:
@ -241,9 +244,27 @@ class MaintenanceSchedule(TransactionBase):
self.validate_maintenance_detail()
self.validate_dates_with_periodicity()
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():
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):
self.db_set("status", "Draft")
@ -341,9 +362,14 @@ class MaintenanceSchedule(TransactionBase):
def on_cancel(self):
for d in self.get("items"):
if d.serial_no:
serial_nos = get_valid_serial_nos(d.serial_no)
self.update_amc_date(serial_nos)
if d.serial_and_batch_bundle:
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.db_set("status", "Cancelled")
delete_events(self.doctype, self.name)
@ -397,11 +423,15 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
target.maintenance_schedule_detail = s_id
def update_serial(source, target, parent):
serial_nos = get_serial_nos(target.serial_no)
if len(serial_nos) == 1:
target.serial_no = serial_nos[0]
else:
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:
target.serial_no = serial_nos[0]
else:
target.serial_no = ""
doclist = get_mapped_doc(
"Maintenance Schedule",

View File

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

View File

@ -16,6 +16,7 @@
"production_item",
"item_name",
"for_quantity",
"serial_and_batch_bundle",
"serial_no",
"column_break_12",
"wip_warehouse",
@ -391,13 +392,17 @@
{
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No"
"hidden": 1,
"label": "Serial No",
"read_only": 1
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch"
"options": "Batch",
"read_only": 1
},
{
"collapsible": 1,
@ -435,6 +440,14 @@
"fieldname": "expected_end_date",
"fieldtype": "Datetime",
"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,

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.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.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@ -672,8 +677,11 @@ class TestWorkOrder(FrappeTestCase):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
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()
@ -682,8 +690,12 @@ class TestWorkOrder(FrappeTestCase):
for row in ste1.get("items"):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
self.assertEqual(row.qty, 10)
remaining_batches.append(row.batch_no)
self.assertEqual(row.qty, 20)
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))
@ -1168,18 +1180,28 @@ class TestWorkOrder(FrappeTestCase):
try:
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.set_work_order_details()
stock_entry.set_serial_no_batch_for_finished_good()
for row in stock_entry.items:
if row.item_code == fg_item:
self.assertTrue(row.serial_no)
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
self.assertTrue(row.serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(row.serial_and_batch_bundle)), sorted(serial_nos)
)
except frappe.MandatoryError:
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(
"Manufacturing Settings",
{"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"
ste_doc = test_stock_entry.make_stock_entry(
item_code=batch_item, target="Stores - _TC", qty=2, 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,
},
item_code=batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
# Inward raw materials in Stores warehouse
ste_doc.insert()
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)
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.items[0].qty = 2
transferred_ste_doc.items[0].batch_no = batch_list[0]
transferred_ste_doc.items[0].qty = 4
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.load_from_db()
# First Manufacture stock entry
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
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)
manufacture_ste_doc1.submit()
# Second Manufacture stock entry
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.assertEqual(manufacture_ste_doc2.items[0].batch_no, batch_list[0])
self.assertEqual(manufacture_ste_doc2.items[0].qty, 1)
self.assertEqual(manufacture_ste_doc2.items[1].batch_no, batch_list[1])
self.assertEqual(manufacture_ste_doc2.items[1].qty, 1)
self.assertTrue(manufacture_ste_doc2.items[0].serial_and_batch_bundle)
bundle_doc = frappe.get_doc(
"Serial and Batch Bundle", manufacture_ste_doc2.items[0].serial_and_batch_bundle
)
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):
frappe.db.set_value(
@ -1386,76 +1411,79 @@ class TestWorkOrder(FrappeTestCase):
fg_item = "Test FG Item with Serial & Batch No Raw Materials"
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
)
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,
},
item_code=sn_batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
# Inward raw materials in Stores warehouse
ste_doc.insert()
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}
batches = list(batch_dict.keys())
serial_nos = []
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)
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.items[0].qty = 2
transferred_ste_doc.items[0].batch_no = batches[0]
transferred_ste_doc.items[0].serial_no = "\n".join(batch_dict.get(batches[0]))
transferred_ste_doc.items[0].qty = 4
transferred_ste_doc.items[0].serial_and_batch_bundle = make_serial_batch_bundle(
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.load_from_db()
# First Manufacture stock entry
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 = manufacture_ste_doc1.items[0].batch_no
self.assertEqual(
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)
bundle = manufacture_ste_doc1.items[0].serial_and_batch_bundle
self.assertTrue(bundle)
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
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
batch_no = manufacture_ste_doc2.items[0].batch_no
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)
bundle = manufacture_ste_doc2.items[0].serial_and_batch_bundle
self.assertTrue(bundle)
batch_no = manufacture_ste_doc2.items[1].batch_no
self.assertEqual(
get_serial_nos(manufacture_ste_doc2.items[1].serial_no)[0], batch_dict.get(batch_no)[0]
)
self.assertEqual(manufacture_ste_doc2.items[1].qty, 1)
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)
serial_nos.remove(d.serial_no)
self.assertFalse(serial_nos)
def test_non_consumed_material_return_against_work_order(self):
frappe.db.set_value(
@ -1490,13 +1518,10 @@ class TestWorkOrder(FrappeTestCase):
for row in ste_doc.items:
row.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
)
row.batch_no = nste_doc.items[0].batch_no
row.serial_no = nste_doc.items[0].serial_no
ste_doc.save()
ste_doc.submit()
ste_doc.load_from_db()
@ -1508,9 +1533,19 @@ class TestWorkOrder(FrappeTestCase):
row.qty -= 2
row.transfer_qty -= 2
if row.serial_no:
serial_nos = get_serial_nos(row.serial_no)
row.serial_no = "\n".join(serial_nos[0:5])
if not row.serial_and_batch_bundle:
continue
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.submit()

View File

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

View File

@ -17,6 +17,7 @@ from frappe.utils import (
get_datetime,
get_link_to_form,
getdate,
now,
nowdate,
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.item.item import get_item_defaults, validate_end_of_life
from erpnext.stock.doctype.serial_no.serial_no import (
auto_make_serial_nos,
clean_serial_no_string,
get_auto_serial_nos,
get_serial_nos,
)
from erpnext.stock.doctype.serial_no.serial_no import get_available_serial_nos, get_serial_nos
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.utilities.transaction_base import validate_uom_is_integer
@ -448,24 +444,53 @@ class WorkOrder(Document):
frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args):
self.serial_no = clean_serial_no_string(self.serial_no)
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
if serial_no_series:
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
item_details = frappe.get_cached_value(
"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
)
if self.serial_no:
args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
auto_make_serial_nos(args)
serial_nos = []
if item_details.serial_no_series:
serial_nos = get_available_serial_nos(item_details.serial_no_series, self.qty)
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,
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):
manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@ -1042,24 +1067,6 @@ class WorkOrder(Document):
bom.set_bom_material_details()
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.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):
if not wo_doc.serial_no:
if not wo_doc.has_serial_no:
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 = []
for d in frappe.get_all(
"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)])
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):
if row.get("qty") <= 0:
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.update_translatable_fields
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.make_quality_inspection_template
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.v14_0.update_company_in_ldc
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

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.calculate_rate_and_amount()
set_expense_account(doc)
doc.make_batches("t_warehouse")
if doc.docstatus == 0:
doc.save()

View File

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

View File

@ -341,10 +341,80 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
}
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);
}
}
);
});
}
});
}
};
cur_frm.add_fetch('project', 'cost_center', 'cost_center');

View File

@ -6,6 +6,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
setup() {
super.setup();
let me = this;
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frappe.flags.hide_serial_batch_dialog = true;
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, 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')) {
this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
return me.set_query_for_batch(doc, cdt, cdn);
if(this.frm.fields_dict["items"].grid.get_field('serial_and_batch_bundle')) {
this.frm.set_query("serial_and_batch_bundle", "items", function(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);
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') {
show_batch_dialog = 1;
}
@ -514,6 +524,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (r.message &&
(r.message.has_batch_no || r.message.has_serial_no)) {
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];
$.each(r.message, function(k, v) {
if(!d[k]) d[k] = v;
@ -538,12 +550,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
d.batch_no = undefined;
}
frappe.flags.dialog_set = true;
erpnext.show_serial_batch_selector(me.frm, d, (item) => {
me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse)
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
me.apply_price_list(item, true);
}, undefined, !frappe.flags.hide_serial_batch_dialog);
} else {
frappe.flags.dialog_set = false;
}
},
() => 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) {
var valid_serial_nos = [];
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_field = "warehouse";
if (frm.doc.is_return) {
if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) {
existing_stock = true;
warehouse = d.warehouse;
warehouse = item_row.warehouse;
} else if (["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) {
receiving_stock = true;
}
@ -2287,11 +2308,24 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_
receiving_stock = true;
} else {
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 {
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() {
new erpnext.SerialNoBatchSelector({
frm: frm,
item: d,
warehouse_details: {
type: "Warehouse",
name: warehouse
},
callback: callback,
on_close: on_close
}, show_dialog);
if (in_list(["Sales Invoice", "Delivery Note"], frm.doc.doctype)) {
item_row.outward = frm.doc.is_return ? 0 : 1;
} else {
item_row.outward = frm.doc.is_return ? 1 : 0;
}
item_row.type_of_transaction = (item_row.outward === 1
? "Outward":"Inward");
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,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:27:51",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:27:51",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"serial_and_batch_bundle",
"serial_no",
"qty",
"description",
"prevdoc_detail_docname",
"prevdoc_docname",
"prevdoc_doctype"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"permlevel": 0,
"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
},
"fieldname": "item_code",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Item Code",
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "serial_no",
"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",
"length": 0,
"no_copy": 0,
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"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,
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"print_hide": 1,
"print_width": "180px",
"width": "180px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "qty",
"fieldtype": "Float",
"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": "Installed Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "qty",
"oldfieldtype": "Currency",
"permlevel": 0,
"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
},
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Installed Qty",
"oldfieldname": "qty",
"oldfieldtype": "Currency",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"in_global_search": 1,
"in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Data",
"print_width": "300px",
"read_only": 1,
"width": "300px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "prevdoc_detail_docname",
"fieldtype": "Data",
"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",
"length": 0,
"no_copy": 1,
"oldfieldname": "prevdoc_detail_docname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "prevdoc_detail_docname",
"fieldtype": "Data",
"hidden": 1,
"label": "Against Document Detail No",
"no_copy": 1,
"oldfieldname": "prevdoc_detail_docname",
"oldfieldtype": "Data",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "prevdoc_docname",
"fieldtype": "Data",
"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",
"length": 0,
"no_copy": 1,
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0,
"fieldname": "prevdoc_docname",
"fieldtype": "Data",
"hidden": 1,
"label": "Against Document No",
"no_copy": 1,
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"width": "150px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "prevdoc_doctype",
"fieldtype": "Data",
"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",
"length": 0,
"no_copy": 1,
"oldfieldname": "prevdoc_doctype",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0,
"fieldname": "prevdoc_doctype",
"fieldtype": "Data",
"hidden": 1,
"label": "Document Type",
"no_copy": 1,
"oldfieldname": "prevdoc_doctype",
"oldfieldtype": "Data",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"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,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"menu_index": 0,
"modified": "2017-02-20 13:24:18.142419",
"modified_by": "Administrator",
"module": "Selling",
"name": "Installation Note Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-03-12 13:47:08.257955",
"modified_by": "Administrator",
"module": "Selling",
"name": "Installation Note Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@ -415,10 +415,17 @@ class SalesOrder(SellingController):
def update_picking_status(self):
total_picked_qty = 0.0
total_qty = 0.0
per_picked = 0.0
for so_item in self.items:
total_picked_qty += flt(so_item.picked_qty)
total_qty += flt(so_item.stock_qty)
per_picked = total_picked_qty / total_qty * 100
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_qty += flt(so_item.stock_qty)
if total_picked_qty and total_qty:
per_picked = total_picked_qty / total_qty * 100
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))
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):
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].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:
from random import randint, uniform

View File

@ -44,7 +44,8 @@ erpnext.PointOfSale.ItemDetails = class {
<div class="item-image"></div>
</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');
@ -53,6 +54,7 @@ erpnext.PointOfSale.ItemDetails = class {
this.$item_image = this.$component.find('.item-image');
this.$form_container = this.$component.find('.form-container');
this.$dicount_section = this.$component.find('.discount-section');
this.$serial_batch_container = this.$component.find('.serial-batch-container');
}
compare_with_current_item(item) {
@ -101,12 +103,9 @@ erpnext.PointOfSale.ItemDetails = class {
const serialized = item_row.has_serial_no;
const batched = item_row.has_batch_no;
const no_serial_selected = !item_row.serial_no;
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))) {
const no_bundle_selected = !item_row.serial_and_batch_bundle;
if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) {
frappe.show_alert({
message: __("Item is removed since no serial / batch no selected."),
indicator: 'orange'
@ -200,13 +199,8 @@ erpnext.PointOfSale.ItemDetails = class {
}
make_auto_serial_selection_btn(item) {
if (item.has_serial_no) {
if (!item.has_batch_no) {
this.$form_container.append(
`<div class="grid-filler no-select"></div>`
);
}
const label = __('Auto Fetch Serial Numbers');
if (item.has_serial_no || item.has_batch_no) {
const label = item.has_serial_no ? __('Select Serial No') : __('Select Batch No');
this.$form_container.append(
`<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() {
this.$form_container.on('click', '.auto-fetch-btn', () => {
this.batch_no_control && this.batch_no_control.set_value('');
let qty = this.qty_control.get_value();
let conversion_factor = this.conversion_factor_control.get_value();
let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : "";
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => {
let frm = this.events.get_frm();
let item_row = this.item_row;
item_row.outward = 1;
item_row.type_of_transaction = "Outward";
let numbers = frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
args: {
qty: qty * conversion_factor,
item_code: this.current_item.item_code,
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);
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) {
frappe.model.set_value(item_row.doctype, item_row.name, {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
});
}
});
});
})
}

View File

@ -196,48 +196,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
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() {
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"));
@ -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() {
super.set_dynamic_labels();
this.set_product_bundle_help(this.frm.doc);
@ -372,52 +300,46 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
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) {
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.
* @param {string} cdt - Document Doctype
* @param {string} cdn - Document name
*/
set_batch_number(cdt, cdn) {
const doc = frappe.get_doc(cdt, cdn);
if (doc && doc.has_batch_no && doc.warehouse) {
this._set_batch_number(doc);
}
}
pick_serial_and_batch(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
_set_batch_number(doc) {
if (doc.batch_no) {
return
}
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 ? "Outward":"Inward";
item.outward = item.qty > 0 ? 1 : 0;
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
}
item.title = item.has_serial_no ?
__("Select Serial No") : __("Select Batch 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);
if (item.has_serial_no && item.has_batch_no) {
item.title = __("Select Serial and Batch");
}
frappe.require(path, function() {
new erpnext.SerialBatchPackageSelector(
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)
});
}
}
);
});
}
}
});
});
}
update_auto_repeat_reference(doc) {

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.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.doctype.incoterm.incoterm import create_incoterms
@ -23,7 +22,6 @@ def after_install():
set_single_defaults()
create_print_setting_custom_fields()
add_all_roles_to("Administrator")
create_default_cash_flow_mapper_templates()
create_default_success_action()
create_default_energy_point_rules()
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():
for success_action in get_default_success_action():
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.auto_indent = 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.save()

View File

@ -486,7 +486,6 @@ def update_stock_settings():
stock_settings.stock_uom = _("Nos")
stock_settings.auto_indent = 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.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;
}
debugger
const section = frm.dashboard.add_section('', __("Stock Levels"));
// sort by qty

View File

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

View File

@ -2,12 +2,14 @@
# License: GNU General Public License v3. See license.txt
from collections import defaultdict
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CombineDatetime, CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime
from frappe.query_builder.functions import CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime, today
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
@ -128,9 +130,7 @@ class Batch(Document):
frappe.throw(_("The selected item cannot have Batch"))
def set_batchwise_valuation(self):
from erpnext.stock.stock_ledger import get_valuation_method
if self.is_new() and get_valuation_method(self.item) != "Moving Average":
if self.is_new():
self.use_batchwise_valuation = 1
def before_save(self):
@ -166,7 +166,12 @@ class Batch(Document):
@frappe.whitelist()
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,
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 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))
)
batchwise_qty = defaultdict(float)
kwargs = frappe._dict(
{
"item_code": item_code,
"warehouse": warehouse,
"posting_date": posting_date,
"posting_time": posting_time,
"batch_no": batch_no,
"ignore_voucher_nos": ignore_voucher_nos,
}
)
if posting_date:
if posting_time is None:
posting_time = nowtime()
batches = get_auto_batch_nos(kwargs)
query = query.where(
CombineDatetime(sle.posting_date, sle.posting_time)
<= CombineDatetime(posting_date, posting_time)
)
if not (batch_no and warehouse):
return batches
out = query.run(as_list=True)[0][0] or 0
for batch in batches:
batchwise_qty[batch.get("batch_no")] += batch.get("qty")
if batch_no and not warehouse:
out = (
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:
out = (
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()
@ -230,13 +222,37 @@ def get_batches_by_oldest(item_code, warehouse):
@frappe.whitelist()
def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
"""Split the batch into a new batch"""
batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert()
qty = flt(qty)
company = frappe.db.get_value(
"Stock Ledger Entry",
dict(item_code=item_code, batch_no=batch_no, warehouse=warehouse),
["company"],
company = frappe.db.get_value("Warehouse", warehouse, "company")
from_bundle_id = make_batch_bundle(
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(
@ -245,8 +261,12 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
purpose="Repack",
company=company,
items=[
dict(item_code=item_code, qty=float(qty or 0), s_warehouse=warehouse, batch_no=batch_no),
dict(item_code=item_code, qty=float(qty or 0), t_warehouse=warehouse, batch_no=batch.name),
dict(
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
def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"):
"""Automatically select `batch_no` for outgoing items in item table"""
for d in doc.get(child_table):
qty = d.get("stock_qty") or d.get("transfer_qty") or d.get("qty") or 0
warehouse = d.get(warehouse_field, None)
if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"):
if not d.batch_no:
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no)
else:
batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse)
if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")):
frappe.throw(
_(
"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"
).format(d.idx, d.batch_no, batch_qty, qty)
)
def make_batch_bundle(kwargs):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
@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))
return (
SerialBatchCreation(
{
"item_code": kwargs.item_code,
"warehouse": kwargs.warehouse,
"posting_date": today(),
"posting_time": nowtime(),
"voucher_type": "Stock Entry",
"qty": flt(kwargs.qty),
"type_of_transaction": kwargs.type_of_transaction,
"company": kwargs.company,
"batches": kwargs.batches,
"do_not_submit": True,
}
)
if throw:
raise UnableToSelectBatchError
return batch_no
.make_serial_and_batch_bundle()
.name
)
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))
def make_batch(args):
if frappe.db.get_value("Item", args.item, "has_batch_no"):
args.doctype = "Batch"
frappe.get_doc(args).insert().name
def make_batch(kwargs):
if frappe.db.get_value("Item", kwargs.item, "has_batch_no"):
kwargs.doctype = "Batch"
return frappe.get_doc(kwargs).insert().name
@frappe.whitelist()
@ -398,3 +393,28 @@ def get_pos_reserved_batch_qty(filters):
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
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": [
{"label": _("Buy"), "items": ["Purchase Invoice", "Purchase Receipt"]},
{"label": _("Sell"), "items": ["Sales Invoice", "Delivery Note"]},
{"label": _("Move"), "items": ["Stock Entry"]},
{"label": _("Move"), "items": ["Serial and Batch Bundle"]},
{"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 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.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.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
BatchNegativeStockError,
)
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.stock_ledger import get_valuation_rate
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
class TestBatch(FrappeTestCase):
@ -49,8 +52,10 @@ class TestBatch(FrappeTestCase):
).insert()
receipt.submit()
self.assertTrue(receipt.items[0].batch_no)
self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), batch_qty)
receipt.load_from_db()
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
@ -80,9 +85,12 @@ class TestBatch(FrappeTestCase):
stock_entry.insert()
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(
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):
@ -91,37 +99,71 @@ class TestBatch(FrappeTestCase):
receipt = self.test_purchase_receipt(batch_qty)
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(
dict(
doctype="Delivery Note",
customer="_Test Customer",
company=receipt.company,
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()
delivery_note.submit()
receipt.load_from_db()
delivery_note.load_from_db()
# shipped from FEFO batch
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"""
receipt = self.test_purchase_receipt(100)
delivery_note = frappe.get_doc(
dict(
doctype="Delivery Note",
customer="_Test Customer",
company=receipt.company,
items=[
dict(item_code="ITEM-BATCH-1", qty=5000, rate=10, warehouse=receipt.items[0].warehouse)
],
)
receipt.load_from_db()
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
sn_doc = SerialBatchCreation(
{
"item_code": "ITEM-BATCH-1",
"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):
"""Test automatic batch selection for outgoing stock entry"""
@ -130,6 +172,24 @@ class TestBatch(FrappeTestCase):
receipt = self.test_purchase_receipt(batch_qty)
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(
dict(
doctype="Stock Entry",
@ -140,6 +200,7 @@ class TestBatch(FrappeTestCase):
item_code=item_code,
qty=batch_qty,
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.insert()
stock_entry.submit()
stock_entry.load_from_db()
# assert same batch is selected
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):
@ -159,11 +221,11 @@ class TestBatch(FrappeTestCase):
receipt = self.test_purchase_receipt()
from erpnext.stock.doctype.batch.batch import split_batch
new_batch = split_batch(
receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22
)
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
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)
def test_get_batch_qty(self):
@ -174,7 +236,10 @@ class TestBatch(FrappeTestCase):
self.assertEqual(
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)
@ -201,6 +266,19 @@ class TestBatch(FrappeTestCase):
)
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(
dict(
doctype="Stock Entry",
@ -210,10 +288,10 @@ class TestBatch(FrappeTestCase):
dict(
item_code=item_name,
qty=90,
serial_and_batch_bundle=sn_doc.name,
t_warehouse=warehouse,
cost_center="Main - _TC",
rate=10,
batch_no=batch_name,
allow_zero_valuation_rate=1,
)
],
@ -320,7 +398,8 @@ class TestBatch(FrappeTestCase):
batches = {}
for rate in rates:
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())
@ -341,7 +420,9 @@ class TestBatch(FrappeTestCase):
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)
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
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):
item_code = "_TestBatchWiseVal"
self.make_batch_item(item_code)
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)
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_2 = make_purchase_receipt(item_code=item_code, qty=1)
self.assertNotEqual(pr_1.items[0].batch_no, pr_2.items[0].batch_no)
self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no)
pr_1.load_from_db()
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):

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.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
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("uom", "qty")
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
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()
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):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:
@ -274,7 +287,12 @@ class DeliveryNote(SellingController):
self.make_gl_entries_on_cancel()
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:
"""Updates Delivered Qty in Stock Reservation Entries."""
@ -1045,8 +1063,6 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"field_map": {
source_document_warehouse_field: target_document_warehouse_field,
"name": "delivery_note_item",
"batch_no": "batch_no",
"serial_no": "serial_no",
"purchase_order": "purchase_order",
"purchase_order_item": "purchase_order_item",
"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.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 (
get_qty_after_transaction,
make_serialized_item,
@ -135,42 +139,6 @@ class TestDeliveryNote(FrappeTestCase):
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):
from frappe.model.naming import make_autoname
@ -178,16 +146,28 @@ class TestDeliveryNote(FrappeTestCase):
{
"doctype": "Serial No",
"item_code": "_Test Serialized Item With Series",
"serial_no": make_autoname("SR", "Serial No"),
"serial_no": make_autoname("SRDD", "Serial No"),
}
)
serial_no.save()
dn = create_delivery_note(
item_code="_Test Serialized Item With Series", serial_no=serial_no.name, do_not_submit=True
bundle_id = make_serial_batch_bundle(
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):
serial_no = frappe.get_doc("Serial No", serial_no)
@ -532,13 +512,14 @@ class TestDeliveryNote(FrappeTestCase):
def test_return_for_serialized_items(self):
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", 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
dn1 = create_delivery_note(
@ -550,23 +531,17 @@ class TestDeliveryNote(FrappeTestCase):
serial_no=serial_no,
)
self.check_serial_no_values(
serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""}
)
self.check_serial_no_values(serial_no, {"warehouse": "_Test Warehouse - _TC"})
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()
self.check_serial_no_values(
serial_no,
{
"warehouse": "_Test Warehouse - _TC",
"delivery_document_no": "",
"purchase_document_no": se.name,
},
{"warehouse": "_Test Warehouse - _TC"},
)
def test_delivery_of_bundled_items_to_target_warehouse(self):
@ -956,7 +931,7 @@ class TestDeliveryNote(FrappeTestCase):
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TESTBATCH.#####",
"batch_number_series": "TESTBATCHIUU.#####",
},
)
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
)
try:
dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
except frappe.ValidationError as e:
if "batch" in str(e).lower():
self.fail("Batch numbers not getting added to bundled items in DN.")
raise e
dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
dn.load_from_db()
self.assertTrue(
"TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item"
)
batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle)
self.assertTrue(batch_no)
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
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)
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()
batch_no = dn.items[0].batch_no
batch_no = get_batch_from_bundle(dn.items[0].serial_and_batch_bundle)
self.assertTrue(batch_no)
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.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(
"items",
{
@ -1249,11 +1250,10 @@ def create_delivery_note(**args):
"qty": args.qty or 1,
"rate": args.rate if args.get("rate") is not None else 100,
"conversion_factor": 1.0,
"serial_and_batch_bundle": bundle_id,
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1,
"expense_account": args.expense_account or "Cost of Goods Sold - _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,
},
)
@ -1262,6 +1262,9 @@ def create_delivery_note(**args):
dn.insert()
if not args.do_not_submit:
dn.submit()
dn.load_from_db()
return dn

View File

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

View File

@ -4,7 +4,7 @@
import frappe
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.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,
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):
@ -297,9 +303,8 @@ class TestLandedCostVoucher(FrappeTestCase):
self.assertEqual(expected_values[gle.account][1], gle.credit)
def test_landed_cost_voucher_for_serialized_item(self):
frappe.db.sql(
"delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')"
)
frappe.db.set_value("Item", "_Test Serialized Item", "serial_no_series", "SNJJ.###")
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
@ -310,17 +315,42 @@ class TestLandedCostVoucher(FrappeTestCase):
)
pr.items[0].item_code = "_Test Serialized Item"
pr.items[0].serial_no = "SN001\nSN002\nSN003\nSN004\nSN005"
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)
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)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no)
self.assertEqual(new_serial_no_rate - serial_no_rate, 5.0)
def test_serialized_lcv_delivered(self):
"""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"
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(
company="_Test Company with perpetual inventory",
warehouse=warehouse,
qty=1,
rate=200,
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
dn = create_delivery_note(
item_code=item_code,
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
serial_no=serial_no,
serial_no=[serial_no],
qty=1,
rate=500,
cost_center="Main - TCP1",
@ -362,14 +413,24 @@ class TestLandedCostVoucher(FrappeTestCase):
charges = 10
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + charges
serial_no = frappe.db.get_value(
"Serial No", serial_no, ["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, 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 Ledger Entry",

View File

@ -19,6 +19,8 @@
"rate",
"uom",
"section_break_9",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"serial_no",
"column_break_11",
"batch_no",
@ -118,7 +120,8 @@
{
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No"
"label": "Serial No",
"read_only": 1
},
{
"fieldname": "column_break_11",
@ -128,7 +131,8 @@
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
"options": "Batch",
"read_only": 1
},
{
"fieldname": "section_break_13",
@ -253,6 +257,19 @@
"no_copy": 1,
"non_negative": 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,

View File

@ -3,6 +3,8 @@
frappe.ui.form.on('Pick List', {
setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
frm.set_indicator_formatter('item_code',
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.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum
from frappe.utils import cint, floor, flt, today
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
from frappe.utils import cint, floor, flt
from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import (
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.serial_batch_bundle import SerialBatchCreation
# 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
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):
self.validate_serial_and_batch_bundle()
self.update_status()
self.update_bundle_picked_qty()
self.update_reference_qty()
self.update_sales_order_picking_status()
def on_cancel(self):
self.ignore_linked_doctypes = "Serial and Batch Bundle"
self.update_status()
self.update_bundle_picked_qty()
self.update_reference_qty()
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 self.docstatus == 0:
status = "Draft"
@ -192,6 +214,7 @@ class PickList(Document):
locations_replica = self.get("locations")
# reset
self.remove_serial_and_batch_bundle()
self.delete_key("locations")
updated_locations = frappe._dict()
for item_doc in items:
@ -265,6 +288,10 @@ class PickList(Document):
for item in locations:
if not item.item_code:
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
reference = item.sales_order_item or item.material_request_item
key = (item_code, item.uom, item.warehouse, item.batch_no, reference)
@ -347,6 +374,7 @@ class PickList(Document):
pi_item.item_code,
pi_item.warehouse,
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_(
"picked_qty"
),
@ -476,18 +504,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
if not stock_qty:
break
serial_nos = None
if item_location.serial_no:
serial_nos = "\n".join(item_location.serial_no[0 : cint(stock_qty)])
locations.append(
frappe._dict(
{
"qty": qty,
"stock_qty": stock_qty,
"warehouse": item_location.warehouse,
"serial_no": serial_nos,
"batch_no": item_location.batch_no,
"serial_and_batch_bundle": item_location.serial_and_batch_bundle,
}
)
)
@ -523,11 +546,7 @@ def get_available_item_locations(
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")
if has_batch_no and 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:
if has_serial_no:
locations = get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
)
@ -553,23 +572,6 @@ def get_available_item_locations(
if picked_item_details:
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:
locations.remove(location)
@ -595,7 +597,7 @@ def get_available_item_locations_for_serialized_item(
frappe.qb.from_(sn)
.select(sn.name, sn.warehouse)
.where((sn.item_code == item_code) & (sn.company == company))
.orderby(sn.purchase_date)
.orderby(sn.creation)
.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)
warehouse_serial_nos_map = frappe._dict()
picked_qty = required_qty
for serial_no, warehouse in serial_nos:
if picked_qty <= 0:
break
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
picked_qty -= 1
locations = []
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
@ -620,63 +649,48 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
):
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
query = (
frappe.qb.from_(sle)
.from_(batch)
.select(sle.warehouse, sle.batch_no, Sum(sle.actual_qty).as_("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())
locations = []
data = get_auto_batch_nos(
frappe._dict(
{
"item_code": item_code,
"warehouse": from_warehouses,
"qty": required_qty + total_picked_qty,
}
)
.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:
query = query.where(sle.warehouse.isin(from_warehouses))
warehouse_wise_batches = frappe._dict()
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(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
):
# Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company
)
bundle_doc = SerialBatchCreation(
{
"item_code": item_code,
"warehouse": warehouse,
"voucher_type": "Pick List",
"total_qty": qty * -1,
"batches": batches,
"type_of_transaction": "Outward",
"company": company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
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)
locations.append(
{
"qty": qty,
"warehouse": warehouse,
"item_code": item_code,
"serial_and_batch_bundle": bundle_doc.name,
}
)
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.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.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_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError,
@ -139,6 +144,18 @@ class TestPickList(FrappeTestCase):
self.assertEqual(pick_list.locations[1].qty, 10)
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(
{
@ -151,7 +168,20 @@ class TestPickList(FrappeTestCase):
"warehouse": "_Test Warehouse - _TC",
"valuation_rate": 100,
"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:
pass
so = make_sales_order(
item_code="_Test Serialized Item", warehouse="_Test Warehouse - _TC", qty=5, rate=1000
)
pick_list = frappe.get_doc(
{
"doctype": "Pick List",
@ -175,18 +209,20 @@ class TestPickList(FrappeTestCase):
"qty": 1000,
"stock_qty": 1000,
"conversion_factor": 1,
"sales_order": "_T-Sales Order-1",
"sales_order_item": "_T-Sales Order-1_item",
"sales_order": so.name,
"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].warehouse, "_Test Warehouse - _TC")
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):
# 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.load_from_db()
oldest_batch_no = pr1.items[0].batch_no
oldest_serial_nos = pr1.items[0].serial_no
oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
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)
@ -267,8 +303,12 @@ class TestPickList(FrappeTestCase):
)
pick_list.set_item_locations()
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
self.assertEqual(pick_list.locations[0].serial_no, oldest_serial_nos)
self.assertEqual(
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()
pr2.cancel()
@ -697,114 +737,3 @@ class TestPickList(FrappeTestCase):
pl.cancel()
pl.reload()
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",
"stock_uom",
"serial_no_and_batch_section",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"serial_no",
"column_break_20",
"batch_no",
@ -72,14 +74,16 @@
"depends_on": "serial_no",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No"
"label": "Serial No",
"read_only": 1
},
{
"depends_on": "batch_no",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
"options": "Batch",
"read_only": 1
},
{
"fieldname": "column_break_2",
@ -187,11 +191,24 @@
"hidden": 1,
"label": "Product Bundle Item",
"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,
"links": [],
"modified": "2022-04-22 05:27:38.497997",
"modified": "2023-03-12 13:50:22.258100",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
@ -202,4 +219,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@ -118,9 +118,7 @@ class PurchaseReceipt(BuyingController):
self.validate_posting_time()
super(PurchaseReceipt, self).validate()
if self._action == "submit":
self.make_batches("warehouse")
else:
if self._action != "submit":
self.set_status()
self.po_required()
@ -242,11 +240,6 @@ class PurchaseReceipt(BuyingController):
# because updating ordered qty, reserved_qty_for_subcontract in bin
# depends upon updated ordered qty in PO
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.repost_future_sle_and_gle()
self.set_consumed_qty_in_subcontract_order()
@ -283,7 +276,12 @@ class PurchaseReceipt(BuyingController):
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
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.set_consumed_qty_in_subcontract_order()

View File

@ -3,7 +3,7 @@
import frappe
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
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.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.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.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}))
pr.load_from_db()
batch_no = pr.items[0].batch_no
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):
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"})
if not item:
@ -206,67 +212,86 @@ class TestPurchaseReceipt(FrappeTestCase):
pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
pr.load_from_db()
serial_nos = frappe.db.get_value(
bundle_id = frappe.db.get_value(
"Stock Ledger Entry",
{"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
pr_different_company = make_purchase_receipt(
item_code=item.name,
qty=2,
rate=500,
serial_no="\n".join(serial_nos),
company="_Test Company 1",
do_not_submit=True,
warehouse="Stores - _TC1",
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item.item_code,
"warehouse": "_Test Warehouse 2 - _TC1",
"company": "_Test Company 1",
"qty": 2,
"voucher_type": "Purchase Receipt",
"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
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()
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)
# Try to receive same serial nos again in the same company with backdated.
pr1 = make_purchase_receipt(
item_code=item.name,
qty=2,
rate=500,
posting_date=posting_date,
serial_no="\n".join(serial_nos),
do_not_submit=True,
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item.item_code,
"warehouse": "_Test Warehouse - _TC",
"company": "_Test Company",
"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.
pr2 = make_purchase_receipt(
item_code=item.name,
qty=2,
rate=500,
posting_date=posting_date,
serial_no="\n".join(serial_nos),
company="_Test Company 1",
do_not_submit=True,
warehouse="Stores - _TC1",
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item.item_code,
"warehouse": "_Test Warehouse 2 - _TC1",
"company": "_Test Company 1",
"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, 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
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
self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel)
# self.assertRaises(SerialNoExistsInFutureTransactionError, dn.cancel)
def test_purchase_receipt_gl_entry(self):
pr = make_purchase_receipt(
@ -307,11 +332,13 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.cancel()
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_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()
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.insert()
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)
for serial_no in accepted_serial_nos:
self.assertEqual(
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)
for serial_no in rejected_serial_nos:
self.assertEqual(
@ -556,23 +586,21 @@ class TestPurchaseReceipt(FrappeTestCase):
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(
serial_no, {"warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name}
)
_check_serial_no_values(serial_no, {"warehouse": "_Test Warehouse - _TC"})
return_pr = make_purchase_receipt(
item_code="_Test Serialized Item With Series",
qty=-1,
is_return=1,
return_against=pr.name,
serial_no=serial_no,
serial_no=[serial_no],
)
_check_serial_no_values(
serial_no,
{"warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name},
{"warehouse": ""},
)
return_pr.cancel()
@ -677,20 +705,23 @@ class TestPurchaseReceipt(FrappeTestCase):
item_code = "Test Manual Created Serial No"
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.load_from_db()
self.assertEqual(
serial_no,
frappe.db.get_value(
"Serial No",
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name},
"name",
),
)
bundle_id = pr_doc.items[0].serial_and_batch_bundle
self.assertEqual(serial_no[0], get_serial_nos_from_bundle(bundle_id)[0])
voucher_no = frappe.db.get_value("Serial and Batch Bundle", bundle_id, "voucher_no")
self.assertEqual(voucher_no, pr_doc.name)
pr_doc.cancel()
# 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.###"))
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]
self.assertEqual(
serial_no,
frappe.db.get_value(
"Serial No",
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name},
"name",
),
)
bundle_id = new_pr_doc.items[0].serial_and_batch_bundle
serial_no = get_serial_nos_from_bundle(bundle_id)[0]
self.assertTrue(serial_no)
voucher_no = frappe.db.get_value("Serial and Batch Bundle", bundle_id, "voucher_no")
self.assertEqual(voucher_no, new_pr_doc.name)
new_pr_doc.cancel()
@ -1491,7 +1521,7 @@ class TestPurchaseReceipt(FrappeTestCase):
)
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)
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"
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(
"items",
{
@ -1931,8 +1985,7 @@ def make_purchase_receipt(**args):
"rate": args.rate if args.rate != None else 50,
"conversion_factor": args.conversion_factor or 1.0,
"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no,
"batch_no": args.batch_no,
"serial_and_batch_bundle": bundle_id,
"stock_uom": args.stock_uom or "_Test UOM",
"uom": uom,
"cost_center": args.cost_center
@ -1958,6 +2011,9 @@ def make_purchase_receipt(**args):
pr.insert()
if not args.do_not_submit:
pr.submit()
pr.load_from_db()
return pr

View File

@ -79,6 +79,7 @@
"purchase_order",
"purchase_invoice",
"column_break_40",
"allow_zero_valuation_rate",
"is_fixed_asset",
"asset_location",
"asset_category",
@ -91,14 +92,19 @@
"delivery_note_item",
"putaway_rule",
"section_break_45",
"allow_zero_valuation_rate",
"bom",
"serial_no",
"add_serial_batch_bundle",
"serial_and_batch_bundle",
"col_break5",
"include_exploded_items",
"batch_no",
"add_serial_batch_for_rejected_qty",
"rejected_serial_and_batch_bundle",
"section_break_3vxt",
"serial_no",
"rejected_serial_no",
"item_tax_rate",
"column_break_tolu",
"batch_no",
"subcontract_bom_section",
"include_exploded_items",
"bom",
"item_weight_details",
"weight_per_unit",
"total_weight",
@ -110,6 +116,7 @@
"manufacturer_part_no",
"accounting_details_section",
"expense_account",
"item_tax_rate",
"column_break_102",
"provisional_expense_account",
"accounting_dimensions_section",
@ -565,37 +572,8 @@
},
{
"fieldname": "section_break_45",
"fieldtype": "Section Break"
},
{
"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
"fieldtype": "Section Break",
"label": "Serial and Batch No"
},
{
"fieldname": "item_tax_template",
@ -1016,12 +994,70 @@
"no_copy": 1,
"print_hide": 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,
"istable": 1,
"links": [],
"modified": "2023-02-28 15:43:04.470104",
"modified": "2023-03-12 13:37:47.778021",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@ -11,7 +11,6 @@ from frappe import _
from frappe.model.document import Document
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
@ -99,7 +98,6 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
item = frappe._dict(item)
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
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)
@ -145,9 +143,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if not qty_to_allocate:
break
updated_table = add_row(
item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos
)
updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, rule.name)
pending_stock_qty -= stock_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
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.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1
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:
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)
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)
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.item.test_item import make_item
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.warehouse.test_warehouse import create_warehouse
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")
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.submit()
pr.load_from_db()
serial_nos = frappe.get_list(
"Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}
)
serial_nos = [d.name for d in serial_nos]
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
serial_nos = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)
stock_entry = make_stock_entry(
item_code="Water Bottle",
source="_Test Warehouse - _TC",
qty=5,
serial_no=serial_nos,
target="Finished Goods - _TC",
purpose="Material Transfer",
apply_putaway_rule=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.load_from_db()
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].putaway_rule, rule_1.name)
self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3]))
self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1")
self.assertEqual(
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].qty, 2)
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(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
self.assertEqual(
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)
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()
pr.cancel()
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