Merge branch 'develop' of https://github.com/frappe/erpnext into #34282-Record-advance-payment-as-a-liability

This commit is contained in:
Deepesh Garg 2023-06-19 09:48:01 +05:30
commit 7ec9d76545
304 changed files with 26024 additions and 14177 deletions

View File

@ -154,7 +154,6 @@
"before": true, "before": true,
"beforeEach": true, "beforeEach": true,
"onScan": true, "onScan": true,
"html2canvas": true,
"extend_cscript": true, "extend_cscript": true,
"localforage": true "localforage": true
} }

View File

@ -3,7 +3,7 @@ import inspect
import frappe import frappe
__version__ = "14.0.0-dev" __version__ = "15.0.0-dev"
def get_default_company(user=None): def get_default_company(user=None):

View File

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

View File

@ -50,13 +50,15 @@ class AccountingDimension(Document):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: else:
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long") frappe.enqueue(
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
)
def on_trash(self): def on_trash(self):
if frappe.flags.in_test: if frappe.flags.in_test:
delete_accounting_dimension(doc=self) delete_accounting_dimension(doc=self)
else: else:
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long") frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
def set_fieldname_and_label(self): def set_fieldname_and_label(self):
if not self.label: if not self.label:

View File

@ -21,8 +21,6 @@
"allow_multi_currency_invoices_against_single_party_account", "allow_multi_currency_invoices_against_single_party_account",
"journals_section", "journals_section",
"merge_similar_account_heads", "merge_similar_account_heads",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
"column_break_18", "column_break_18",
@ -36,6 +34,7 @@
"book_tax_discount_loss", "book_tax_discount_loss",
"print_settings", "print_settings",
"show_inclusive_tax_in_print", "show_inclusive_tax_in_print",
"show_taxes_as_table_in_print",
"column_break_12", "column_break_12",
"show_payment_schedule_in_print", "show_payment_schedule_in_print",
"currency_exchange_section", "currency_exchange_section",
@ -173,13 +172,6 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Stale Days" "label": "Stale Days"
}, },
{
"default": "0",
"description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow",
"fieldtype": "Check",
"label": "Enable Custom Cash Flow Format"
},
{ {
"default": "0", "default": "0",
"description": "Payment Terms from orders will be fetched into the invoices as is", "description": "Payment Terms from orders will be fetched into the invoices as is",
@ -338,11 +330,6 @@
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "POS" "label": "POS"
}, },
{
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
},
{ {
"default": "0", "default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
@ -390,6 +377,12 @@
"fieldname": "auto_reconcile_payments", "fieldname": "auto_reconcile_payments",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Auto Reconcile Payments" "label": "Auto Reconcile Payments"
},
{
"default": "0",
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -397,7 +390,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-04-21 13:11:37.130743", "modified": "2023-06-13 18:47:46.430291",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -41,7 +41,7 @@ frappe.ui.form.on("Bank Clearance", {
frm.trigger("get_payment_entries") frm.trigger("get_payment_entries")
); );
frm.change_custom_button_type('Get Payment Entries', null, 'primary'); frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary');
}, },
update_clearance_date: function(frm) { update_clearance_date: function(frm) {
@ -53,8 +53,8 @@ frappe.ui.form.on("Bank Clearance", {
frm.refresh_fields(); frm.refresh_fields();
if (!frm.doc.payment_entries.length) { if (!frm.doc.payment_entries.length) {
frm.change_custom_button_type('Get Payment Entries', null, 'primary'); frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary');
frm.change_custom_button_type('Update Clearance Date', null, 'default'); frm.change_custom_button_type(__('Update Clearance Date'), null, 'default');
} }
} }
}); });
@ -72,8 +72,8 @@ frappe.ui.form.on("Bank Clearance", {
frm.trigger("update_clearance_date") frm.trigger("update_clearance_date")
); );
frm.change_custom_button_type('Get Payment Entries', null, 'default'); frm.change_custom_button_type(__('Get Payment Entries'), null, 'default');
frm.change_custom_button_type('Update Clearance Date', null, 'primary'); frm.change_custom_button_type(__('Update Clearance Date'), null, 'primary');
} }
} }
}); });

View File

@ -81,7 +81,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.add_custom_button(__('Get Unreconciled Entries'), function() { frm.add_custom_button(__('Get Unreconciled Entries'), function() {
frm.trigger("make_reconciliation_tool"); frm.trigger("make_reconciliation_tool");
}); });
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary'); frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'primary');
}, },

View File

@ -125,14 +125,27 @@ def validate_expense_against_budget(args, expense_amount=0):
if not args.account: if not args.account:
return return
for budget_against in ["project", "cost_center"] + get_accounting_dimensions(): default_dimensions = [
{
"fieldname": "project",
"document_type": "Project",
},
{
"fieldname": "cost_center",
"document_type": "Cost Center",
},
]
for dimension in default_dimensions + get_accounting_dimensions(as_list=False):
budget_against = dimension.get("fieldname")
if ( if (
args.get(budget_against) args.get(budget_against)
and args.account and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"}) and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
): ):
doctype = frappe.unscrub(budget_against) doctype = dimension.get("document_type")
if frappe.get_cached_value("DocType", doctype, "is_tree"): if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])

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

@ -245,6 +245,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -315,10 +316,11 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-03 18:55:43.683053", "modified": "2023-06-03 16:24:01.677026",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning", "name": "Dunning",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -365,6 +367,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"title_field": "customer_name", "title_field": "customer_name",
"track_changes": 1 "track_changes": 1
} }

View File

@ -35,6 +35,21 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
} }
}, },
validate_rounding_loss: function(frm) {
let allowance = frm.doc.rounding_loss_allowance;
if (!(allowance > 0 && allowance < 1)) {
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
}
},
rounding_loss_allowance: function(frm) {
frm.events.validate_rounding_loss(frm);
},
validate: function(frm) {
frm.events.validate_rounding_loss(frm);
},
get_entries: function(frm, account) { get_entries: function(frm, account) {
frappe.call({ frappe.call({
method: "get_accounts_data", method: "get_accounts_data",
@ -126,7 +141,8 @@ var get_account_details = function(frm, cdt, cdn) {
company: frm.doc.company, company: frm.doc.company,
posting_date: frm.doc.posting_date, posting_date: frm.doc.posting_date,
party_type: row.party_type, party_type: row.party_type,
party: row.party party: row.party,
rounding_loss_allowance: frm.doc.rounding_loss_allowance
}, },
callback: function(r){ callback: function(r){
$.extend(row, r.message); $.extend(row, r.message);

View File

@ -8,6 +8,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"posting_date", "posting_date",
"rounding_loss_allowance",
"column_break_2", "column_break_2",
"company", "company",
"section_break_4", "section_break_4",
@ -96,11 +97,18 @@
{ {
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0.05",
"description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
"fieldname": "rounding_loss_allowance",
"fieldtype": "Float",
"label": "Rounding Loss Allowance"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-29 19:38:24.416529", "modified": "2023-06-12 21:02:09.818208",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Exchange Rate Revaluation", "name": "Exchange Rate Revaluation",

View File

@ -12,13 +12,19 @@ from frappe.utils import flt, get_link_to_form
import erpnext import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
from erpnext.accounts.utils import get_currency_precision
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
class ExchangeRateRevaluation(Document): class ExchangeRateRevaluation(Document):
def validate(self): def validate(self):
self.validate_rounding_loss_allowance()
self.set_total_gain_loss() self.set_total_gain_loss()
def validate_rounding_loss_allowance(self):
if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1):
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
def set_total_gain_loss(self): def set_total_gain_loss(self):
total_gain_loss = 0 total_gain_loss = 0
@ -91,7 +97,12 @@ class ExchangeRateRevaluation(Document):
def get_accounts_data(self): def get_accounts_data(self):
self.validate_mandatory() self.validate_mandatory()
account_details = self.get_account_balance_from_gle( account_details = self.get_account_balance_from_gle(
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None company=self.company,
posting_date=self.posting_date,
account=None,
party_type=None,
party=None,
rounding_loss_allowance=self.rounding_loss_allowance,
) )
accounts_with_new_balance = self.calculate_new_account_balance( accounts_with_new_balance = self.calculate_new_account_balance(
self.company, self.posting_date, account_details self.company, self.posting_date, account_details
@ -103,7 +114,9 @@ class ExchangeRateRevaluation(Document):
return accounts_with_new_balance return accounts_with_new_balance
@staticmethod @staticmethod
def get_account_balance_from_gle(company, posting_date, account, party_type, party): def get_account_balance_from_gle(
company, posting_date, account, party_type, party, rounding_loss_allowance
):
account_details = [] account_details = []
if company and posting_date: if company and posting_date:
@ -170,6 +183,23 @@ class ExchangeRateRevaluation(Document):
.run(as_dict=True) .run(as_dict=True)
) )
# round off balance based on currency precision
# and consider debit-credit difference allowance
currency_precision = get_currency_precision()
rounding_loss_allowance = float(rounding_loss_allowance) or 0.05
for acc in account_details:
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:
acc.balance_in_account_currency = 0
acc.balance = flt(acc.balance, currency_precision)
if abs(acc.balance) <= rounding_loss_allowance:
acc.balance = 0
acc.zero_balance = (
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
)
return account_details return account_details
@staticmethod @staticmethod
@ -521,7 +551,9 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
@frappe.whitelist() @frappe.whitelist()
def get_account_details(company, posting_date, account, party_type=None, party=None): def get_account_details(
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float = None
):
if not (company and posting_date): if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory")) frappe.throw(_("Company and Posting Date is mandatory"))
@ -539,7 +571,12 @@ def get_account_details(company, posting_date, account, party_type=None, party=N
"account_currency": account_currency, "account_currency": account_currency,
} }
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle( account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party company=company,
posting_date=posting_date,
account=account,
party_type=party_type,
party=party,
rounding_loss_allowance=rounding_loss_allowance,
) )
if account_balance and ( if account_balance and (

View File

@ -12,7 +12,7 @@ from frappe.utils import add_days, add_years, cstr, getdate
class FiscalYear(Document): class FiscalYear(Document):
@frappe.whitelist() @frappe.whitelist()
def set_as_default(self): def set_as_default(self):
frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name) frappe.db.set_single_value("Global Defaults", "current_fiscal_year", self.name)
global_defaults = frappe.get_doc("Global Defaults") global_defaults = frappe.get_doc("Global Defaults")
global_defaults.check_permission("write") global_defaults.check_permission("write")
global_defaults.on_update() global_defaults.on_update()

View File

@ -575,7 +575,7 @@ $.extend(erpnext.journal_entry, {
}; };
if(!frm.doc.multi_currency) { if(!frm.doc.multi_currency) {
$.extend(filters, { $.extend(filters, {
account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency account_currency: ['in', [frappe.get_doc(":Company", frm.doc.company).default_currency, null]]
}); });
} }
return { filters: filters }; return { filters: filters };

View File

@ -952,6 +952,7 @@ class JournalEntry(AccountsController):
blank_row.debit_in_account_currency = abs(diff) blank_row.debit_in_account_currency = abs(diff)
blank_row.debit = abs(diff) blank_row.debit = abs(diff)
self.set_total_debit_credit()
self.validate_total_debit_and_credit() self.validate_total_debit_and_credit()
@frappe.whitelist() @frappe.whitelist()

View File

@ -105,8 +105,8 @@ class TestJournalEntry(unittest.TestCase):
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]: elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher # if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
frappe.db.set_value( frappe.db.set_single_value(
"Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0
) )
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name) submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel) self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)

View File

@ -28,7 +28,7 @@ frappe.ui.form.on("Journal Entry Template", {
if(!frm.doc.multi_currency) { if(!frm.doc.multi_currency) {
$.extend(filters, { $.extend(filters, {
account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency account_currency: ['in', [frappe.get_doc(":Company", frm.doc.company).default_currency, null]]
}); });
} }

View File

@ -178,19 +178,57 @@ class PaymentEntry(AccountsController):
) )
def validate_allocated_amount(self): def validate_allocated_amount(self):
for d in self.get("references"): if self.payment_type == "Internal Transfer":
return
latest_references = get_outstanding_reference_documents(
{
"posting_date": self.posting_date,
"company": self.company,
"party_type": self.party_type,
"payment_type": self.payment_type,
"party": self.party,
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
}
)
# Group latest_references by (voucher_type, voucher_no)
latest_lookup = {}
for d in latest_references:
d = frappe._dict(d)
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
for d in self.get("references").copy():
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
# The reference has already been fully paid
if not latest:
frappe.throw(
_("{0} {1} has already been fully paid.").format(d.reference_doctype, d.reference_name)
)
# The reference has already been partly paid
elif (
latest.outstanding_amount < latest.invoice_amount
and d.outstanding_amount != latest.outstanding_amount
):
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' button to get the latest outstanding amount."
).format(d.reference_doctype, d.reference_name)
)
d.outstanding_amount = latest.outstanding_amount
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
if (flt(d.allocated_amount)) > 0: if (flt(d.allocated_amount)) > 0:
if flt(d.allocated_amount) > flt(d.outstanding_amount): if flt(d.allocated_amount) > flt(d.outstanding_amount):
frappe.throw( frappe.throw(fail_message.format(d.idx))
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
)
# Check for negative outstanding invoices as well # Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0: if flt(d.allocated_amount) < 0:
if flt(d.allocated_amount) < flt(d.outstanding_amount): if flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw( frappe.throw(fail_message.format(d.idx))
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
)
def delink_advance_entry_references(self): def delink_advance_entry_references(self):
for reference in self.references: for reference in self.references:
@ -396,7 +434,7 @@ class PaymentEntry(AccountsController):
for k, v in no_oustanding_refs.items(): for k, v in no_oustanding_refs.items():
frappe.msgprint( frappe.msgprint(
_( _(
"{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry." "{} - {} now has {} as it had no outstanding amount left before submitting the Payment Entry."
).format( ).format(
_(k), _(k),
frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(", ".join(d.reference_name for d in v)),
@ -1535,7 +1573,7 @@ def get_orders_to_be_billed(
if voucher_type: if voucher_type:
doc = frappe.get_doc({"doctype": voucher_type}) doc = frappe.get_doc({"doctype": voucher_type})
condition = "" condition = ""
if cost_center and doc and hasattr(doc, "cost_center"): if doc and hasattr(doc, "cost_center") and doc.cost_center:
condition = " and cost_center='%s'" % cost_center condition = " and cost_center='%s'" % cost_center
orders = [] orders = []
@ -1581,13 +1619,15 @@ def get_orders_to_be_billed(
order_list = [] order_list = []
for d in orders: for d in orders:
if filters.get("oustanding_amt_greater_than") and flt(d.outstanding_amount) < flt( if (
filters.get("outstanding_amt_greater_than") filters
): and filters.get("outstanding_amt_greater_than")
continue and filters.get("outstanding_amt_less_than")
and not (
if filters.get("oustanding_amt_less_than") and flt(d.outstanding_amount) > flt( flt(filters.get("outstanding_amt_greater_than"))
filters.get("outstanding_amt_less_than") <= flt(d.outstanding_amount)
<= flt(filters.get("outstanding_amt_less_than"))
)
): ):
continue continue

View File

@ -1013,6 +1013,30 @@ class TestPaymentEntry(FrappeTestCase):
employee = make_employee("test_payment_entry@salary.com", company="_Test Company") employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
create_payment_entry(party_type="Employee", party=employee, save=True) create_payment_entry(party_type="Employee", party=employee, save=True)
def test_duplicate_payment_entry_allocate_amount(self):
si = create_sales_invoice()
pe_draft = get_payment_entry("Sales Invoice", si.name)
pe_draft.insert()
pe = get_payment_entry("Sales Invoice", si.name)
pe.submit()
self.assertRaises(frappe.ValidationError, pe_draft.submit)
def test_duplicate_payment_entry_partial_allocate_amount(self):
si = create_sales_invoice()
pe_draft = get_payment_entry("Sales Invoice", si.name)
pe_draft.insert()
pe = get_payment_entry("Sales Invoice", si.name)
pe.received_amount = si.total / 2
pe.references[0].allocated_amount = si.total / 2
pe.submit()
self.assertRaises(frappe.ValidationError, pe_draft.submit)
def create_payment_entry(**args): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")

View File

@ -75,22 +75,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.add_custom_button(__('Get Unreconciled Entries'), () => this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
this.frm.trigger("get_unreconciled_entries") this.frm.trigger("get_unreconciled_entries")
); );
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary'); this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'primary');
} }
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) { if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
this.frm.add_custom_button(__('Allocate'), () => this.frm.add_custom_button(__('Allocate'), () =>
this.frm.trigger("allocate") this.frm.trigger("allocate")
); );
this.frm.change_custom_button_type('Allocate', null, 'primary'); this.frm.change_custom_button_type(__('Allocate'), null, 'primary');
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default'); this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'default');
} }
if (this.frm.doc.allocation.length) { if (this.frm.doc.allocation.length) {
this.frm.add_custom_button(__('Reconcile'), () => this.frm.add_custom_button(__('Reconcile'), () =>
this.frm.trigger("reconcile") this.frm.trigger("reconcile")
); );
this.frm.change_custom_button_type('Reconcile', null, 'primary'); this.frm.change_custom_button_type(__('Reconcile'), null, 'primary');
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default'); this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'default');
this.frm.change_custom_button_type('Allocate', null, 'default'); this.frm.change_custom_button_type(__('Allocate'), null, 'default');
} }
// check for any running reconciliation jobs // check for any running reconciliation jobs

View File

@ -6,7 +6,6 @@ import frappe
from frappe import _, msgprint, qb from frappe import _, msgprint, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
import erpnext import erpnext
@ -144,12 +143,29 @@ class PaymentReconciliation(Document):
return list(journal_entries) return list(journal_entries)
def get_return_invoices(self):
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doc = qb.DocType(voucher_type)
self.return_invoices = (
qb.from_(doc)
.select(
ConstantColumn(voucher_type).as_("voucher_type"),
doc.name.as_("voucher_no"),
doc.return_against,
)
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
)
.run(as_dict=True)
)
def get_dr_or_cr_notes(self): def get_dr_or_cr_notes(self):
self.build_qb_filter_conditions(get_return_invoices=True) self.build_qb_filter_conditions(get_return_invoices=True)
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
if erpnext.get_party_account_type(self.party_type) == "Receivable": if erpnext.get_party_account_type(self.party_type) == "Receivable":
self.common_filter_conditions.append(ple.account_type == "Receivable") self.common_filter_conditions.append(ple.account_type == "Receivable")
@ -157,19 +173,10 @@ class PaymentReconciliation(Document):
self.common_filter_conditions.append(ple.account_type == "Payable") self.common_filter_conditions.append(ple.account_type == "Payable")
self.common_filter_conditions.append(ple.account == self.receivable_payable_account) self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
# get return invoices self.get_return_invoices()
doc = qb.DocType(voucher_type) return_invoices = [
return_invoices = ( x for x in self.return_invoices if x.return_against == None or x.return_against == ""
qb.from_(doc) ]
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
& (IfNull(doc.return_against, "") == "")
)
.run(as_dict=True)
)
outstanding_dr_or_cr = [] outstanding_dr_or_cr = []
if return_invoices: if return_invoices:
@ -221,6 +228,15 @@ class PaymentReconciliation(Document):
accounting_dimensions=self.accounting_dimension_filter_conditions, accounting_dimensions=self.accounting_dimension_filter_conditions,
) )
cr_dr_notes = (
[x.voucher_no for x in self.return_invoices]
if self.party_type in ["Customer", "Supplier"]
else []
)
# Filter out cr/dr notes from outstanding invoices list
# Happens when non-standalone cr/dr notes are linked with another invoice through journal entry
non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes]
if self.invoice_limit: if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]

View File

@ -44,6 +44,7 @@ class PeriodClosingVoucher(AccountsController):
voucher_type="Period Closing Voucher", voucher_type="Period Closing Voucher",
voucher_no=self.name, voucher_no=self.name,
queue="long", queue="long",
enqueue_after_commit=True,
) )
frappe.msgprint( frappe.msgprint(
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True _("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True

View File

@ -442,6 +442,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1554,11 +1555,10 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-09-30 03:49:50.455199", "modified": "2023-06-03 16:23:41.083409",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -469,7 +469,7 @@
"options": "UOM" "options": "UOM"
}, },
{ {
"description": "If rate is zero them item will be treated as \"Free Item\"", "description": "If rate is zero then item will be treated as \"Free Item\"",
"fieldname": "free_item_rate", "fieldname": "free_item_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Free Item Rate" "label": "Free Item Rate"

View File

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

View File

@ -158,7 +158,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "customer_name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]], filters=[["disabled", "=", 0], [fields_dict[customer_collection], "IN", selected]],
) )

View File

@ -443,12 +443,14 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact Email", "label": "Contact Email",
"options": "Email",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1574,7 +1576,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-05 17:40:35.320635", "modified": "2023-06-03 16:21:54.637245",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

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

View File

@ -26,6 +26,11 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_taxes, get_taxes,
make_purchase_receipt, make_purchase_receipt,
) )
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
from erpnext.stock.tests.test_utils import StockTestMixin from erpnext.stock.tests.test_utils import StockTestMixin
@ -37,7 +42,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
unlink_payment_on_cancel_of_invoice() unlink_payment_on_cancel_of_invoice()
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1)
@classmethod @classmethod
def tearDownClass(self): def tearDownClass(self):
@ -637,13 +642,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
gle_filters={"account": "Stock In Hand - TCP1"}, gle_filters={"account": "Stock In Hand - TCP1"},
) )
# assert loss booked in COGS
self.assertGLEs(
return_pi,
[{"credit": 0, "debit": 200}],
gle_filters={"account": "Cost of Goods Sold - TCP1"},
)
def test_return_with_lcv(self): def test_return_with_lcv(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
@ -888,14 +886,20 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
rejected_warehouse="_Test Rejected Warehouse - _TC", rejected_warehouse="_Test Rejected Warehouse - _TC",
allow_zero_valuation_rate=1, allow_zero_valuation_rate=1,
) )
pi.load_from_db()
serial_no = get_serial_nos_from_bundle(pi.get("items")[0].serial_and_batch_bundle)[0]
rejected_serial_no = get_serial_nos_from_bundle(
pi.get("items")[0].rejected_serial_and_batch_bundle
)[0]
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), frappe.db.get_value("Serial No", serial_no, "warehouse"),
pi.get("items")[0].warehouse, pi.get("items")[0].warehouse,
) )
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, "warehouse"), frappe.db.get_value("Serial No", rejected_serial_no, "warehouse"),
pi.get("items")[0].rejected_warehouse, pi.get("items")[0].rejected_warehouse,
) )
@ -1221,9 +1225,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
) )
frappe.db.set_value( frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value( frappe.db.set_value(
@ -1358,8 +1360,8 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pay.reload() pay.reload()
pay.cancel() pay.cancel()
frappe.db.set_value( frappe.db.set_single_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
) )
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
@ -1652,7 +1654,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
) )
pi.load_from_db() pi.load_from_db()
batch_no = pi.items[0].batch_no batch_no = get_batch_from_bundle(pi.items[0].serial_and_batch_bundle)
self.assertTrue(batch_no) self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
@ -1706,6 +1708,21 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
set_advance_flag(company="_Test Company", flag=0, default_account="") set_advance_flag(company="_Test Company", flag=0, default_account="")
def test_gl_entries_for_standalone_debit_note(self):
make_purchase_invoice(qty=5, rate=500, update_stock=True)
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
# override the rate with valuation rate
sle = frappe.get_all(
"Stock Ledger Entry",
fields=["stock_value_difference", "actual_qty"],
filters={"voucher_no": returned_inv.name},
)[0]
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
def set_advance_flag(company, flag, default_account): def set_advance_flag(company, flag, default_account):
frappe.db.set_value( frappe.db.set_value(
@ -1794,6 +1811,32 @@ def make_purchase_invoice(**args):
pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
pi.cost_center = args.parent_cost_center pi.cost_center = args.parent_cost_center
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
batches = {}
qty = args.qty or 5
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
serial_nos = args.get("serial_no") or []
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Purchase Invoice",
"serial_nos": serial_nos,
"type_of_transaction": "Inward",
"posting_date": args.posting_date or today(),
"posting_time": args.posting_time,
}
)
).name
pi.append( pi.append(
"items", "items",
{ {
@ -1808,12 +1851,11 @@ def make_purchase_invoice(**args):
"discount_account": args.discount_account or None, "discount_account": args.discount_account or None,
"discount_amount": args.discount_amount or 0, "discount_amount": args.discount_amount or 0,
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_no": args.serial_no, "serial_and_batch_bundle": bundle_id,
"stock_uom": args.uom or "_Test UOM", "stock_uom": args.uom or "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or "", "asset_location": args.location or "",
"allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0,
}, },
@ -1857,6 +1899,31 @@ def make_purchase_invoice_against_cost_center(**args):
if args.supplier_warehouse: if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
batches = {}
qty = args.qty or 5
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
serial_nos = args.get("serial_no") or []
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Purchase Receipt",
"serial_nos": serial_nos,
"posting_date": args.posting_date or today(),
"posting_time": args.posting_time,
}
)
).name
pi.append( pi.append(
"items", "items",
{ {
@ -1867,12 +1934,11 @@ def make_purchase_invoice_against_cost_center(**args):
"rejected_qty": args.rejected_qty or 0, "rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50, "rate": args.rate or 50,
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_no": args.serial_no, "serial_and_batch_bundle": bundle_id,
"stock_uom": "_Test UOM", "stock_uom": "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
}, },
) )
if not args.do_not_save: if not args.do_not_save:

View File

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

View File

@ -520,6 +520,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -2154,7 +2155,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-04-28 14:15:59.901154", "modified": "2023-06-03 16:22:16.219333",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -39,13 +39,8 @@ from erpnext.controllers.accounts_controller import (
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.setup.doctype.company.company import update_company_current_month_sales from erpnext.setup.doctype.company.company import update_company_current_month_sales
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import ( from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
get_delivery_note_serial_no,
get_serial_nos,
update_serial_nos_after_submit,
)
form_grid_templates = {"items": "templates/form_grid/item_grid.html"} form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@ -132,9 +127,6 @@ class SalesInvoice(SellingController):
if not self.is_opening: if not self.is_opening:
self.is_opening = "No" self.is_opening = "No"
if self._action != "submit" and self.update_stock and not self.is_return:
set_batch_nos(self, "warehouse", True)
if self.redeem_loyalty_points: if self.redeem_loyalty_points:
lp = frappe.get_doc("Loyalty Program", self.loyalty_program) lp = frappe.get_doc("Loyalty Program", self.loyalty_program)
self.loyalty_redemption_account = ( self.loyalty_redemption_account = (
@ -265,8 +257,6 @@ class SalesInvoice(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO # because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
if self.is_return and self.update_stock:
update_serial_nos_after_submit(self, "items")
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
@ -279,8 +269,6 @@ class SalesInvoice(SellingController):
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit() self.check_credit_limit()
self.update_serial_no()
if not cint(self.is_pos) == 1 and not self.is_return: if not cint(self.is_pos) == 1 and not self.is_return:
self.update_against_document_in_jv() self.update_against_document_in_jv()
@ -364,7 +352,6 @@ class SalesInvoice(SellingController):
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note") self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.update_serial_no(in_cancel=True)
# Updating stock ledger should always be called after updating prevdoc status, # Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO # because updating reserved qty in bin depends upon updated delivered qty in SO
@ -403,6 +390,7 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger", "Repost Payment Ledger",
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Payment Ledger Entry", "Payment Ledger Entry",
"Serial and Batch Bundle",
) )
def update_status_updater_args(self): def update_status_updater_args(self):
@ -1016,10 +1004,16 @@ class SalesInvoice(SellingController):
def check_prev_docstatus(self): def check_prev_docstatus(self):
for d in self.get("items"): for d in self.get("items"):
if d.sales_order and frappe.db.get_value("Sales Order", d.sales_order, "docstatus") != 1: if (
d.sales_order
and frappe.db.get_value("Sales Order", d.sales_order, "docstatus", cache=True) != 1
):
frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order)) frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order))
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1: if (
d.delivery_note
and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus", cache=True) != 1
):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def make_gl_entries(self, gl_entries=None, from_repost=False): def make_gl_entries(self, gl_entries=None, from_repost=False):
@ -1529,20 +1523,6 @@ class SalesInvoice(SellingController):
self.set("write_off_amount", reference_doc.get("write_off_amount")) self.set("write_off_amount", reference_doc.get("write_off_amount"))
self.due_date = None self.due_date = None
def update_serial_no(self, in_cancel=False):
"""update Sales Invoice refrence in Serial No"""
invoice = None if (in_cancel or self.is_return) else self.name
if in_cancel and self.is_return:
invoice = self.return_against
for item in self.items:
if not item.serial_no:
continue
for serial_no in get_serial_nos(item.serial_no):
if serial_no and frappe.db.get_value("Serial No", serial_no, "item_code") == item.item_code:
frappe.db.set_value("Serial No", serial_no, "sales_invoice", invoice)
def validate_serial_numbers(self): def validate_serial_numbers(self):
""" """
validate serial number agains Delivery Note and Sales Invoice validate serial number agains Delivery Note and Sales Invoice

View File

@ -30,6 +30,11 @@ from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from erpnext.stock.doctype.stock_entry.test_stock_entry import ( from erpnext.stock.doctype.stock_entry.test_stock_entry import (
get_qty_after_transaction, get_qty_after_transaction,
@ -1058,7 +1063,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos.write_off_amount, 10) self.assertEqual(pos.write_off_amount, 10)
def test_pos_with_no_gl_entry_for_change_amount(self): def test_pos_with_no_gl_entry_for_change_amount(self):
frappe.db.set_value("Accounts Settings", None, "post_change_gl_entries", 0) frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 0)
make_pos_profile( make_pos_profile(
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
@ -1108,7 +1113,7 @@ class TestSalesInvoice(unittest.TestCase):
self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True) self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
frappe.db.set_value("Accounts Settings", None, "post_change_gl_entries", 1) frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 1)
def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False): def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
if validate_without_change_gle: if validate_without_change_gle:
@ -1348,55 +1353,47 @@ class TestSalesInvoice(unittest.TestCase):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item() se = make_serialized_item()
serial_nos = get_serial_nos(se.get("items")[0].serial_no) se.load_from_db()
serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
si = frappe.copy_doc(test_records[0]) si = frappe.copy_doc(test_records[0])
si.update_stock = 1 si.update_stock = 1
si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].item_code = "_Test Serialized Item With Series"
si.get("items")[0].qty = 1 si.get("items")[0].qty = 1
si.get("items")[0].serial_no = serial_nos[0] si.get("items")[0].warehouse = se.get("items")[0].t_warehouse
si.get("items")[0].serial_and_batch_bundle = make_serial_batch_bundle(
frappe._dict(
{
"item_code": si.get("items")[0].item_code,
"warehouse": si.get("items")[0].warehouse,
"company": si.company,
"qty": 1,
"voucher_type": "Stock Entry",
"serial_nos": [serial_nos[0]],
"posting_date": si.posting_date,
"posting_time": si.posting_time,
"type_of_transaction": "Outward",
"do_not_submit": True,
}
)
).name
si.insert() si.insert()
si.submit() si.submit()
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name
)
return si return si
def test_serialized_cancel(self): def test_serialized_cancel(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
si = self.test_serialized() si = self.test_serialized()
si.cancel() si.cancel()
serial_nos = get_serial_nos(si.get("items")[0].serial_no) serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
) )
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"))
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"))
def test_serialize_status(self):
serial_no = frappe.get_doc(
{
"doctype": "Serial No",
"item_code": "_Test Serialized Item With Series",
"serial_no": make_autoname("SR", "Serial No"),
}
)
serial_no.save()
si = frappe.copy_doc(test_records[0])
si.update_stock = 1
si.get("items")[0].item_code = "_Test Serialized Item With Series"
si.get("items")[0].qty = 1
si.get("items")[0].serial_no = serial_no.name
si.insert()
self.assertRaises(SerialNoWarehouseError, si.submit)
def test_serial_numbers_against_delivery_note(self): def test_serial_numbers_against_delivery_note(self):
""" """
@ -1404,20 +1401,22 @@ class TestSalesInvoice(unittest.TestCase):
serial numbers are same serial numbers are same
""" """
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item() se = make_serialized_item()
serial_nos = get_serial_nos(se.get("items")[0].serial_no) se.load_from_db()
serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=serial_nos[0]) dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=[serial_nos])
dn.submit() dn.submit()
dn.load_from_db()
serial_nos = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0]
self.assertTrue(get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0])
si = make_sales_invoice(dn.name) si = make_sales_invoice(dn.name)
si.save() si.save()
self.assertEqual(si.get("items")[0].serial_no, dn.get("items")[0].serial_no)
def test_return_sales_invoice(self): def test_return_sales_invoice(self):
make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100)
@ -2453,7 +2452,7 @@ class TestSalesInvoice(unittest.TestCase):
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse." "Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
# setup # setup
old_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") old_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled("_Test Company 1") old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled("_Test Company 1")
frappe.local.enable_perpetual_inventory["_Test Company 1"] = 1 frappe.local.enable_perpetual_inventory["_Test Company 1"] = 1
@ -2507,7 +2506,7 @@ class TestSalesInvoice(unittest.TestCase):
# tear down # tear down
frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock) frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock)
def test_sle_for_target_warehouse(self): def test_sle_for_target_warehouse(self):
se = make_stock_entry( se = make_stock_entry(
@ -2573,7 +2572,7 @@ class TestSalesInvoice(unittest.TestCase):
"posting_date": si.posting_date, "posting_date": si.posting_date,
"posting_time": si.posting_time, "posting_time": si.posting_time,
"qty": -1 * flt(d.get("stock_qty")), "qty": -1 * flt(d.get("stock_qty")),
"serial_no": d.serial_no, "serial_and_batch_bundle": d.serial_and_batch_bundle,
"company": si.company, "company": si.company,
"voucher_type": "Sales Invoice", "voucher_type": "Sales Invoice",
"voucher_no": si.name, "voucher_no": si.name,
@ -2899,7 +2898,7 @@ class TestSalesInvoice(unittest.TestCase):
party_link = create_party_link("Supplier", supplier, customer) party_link = create_party_link("Supplier", supplier, customer)
# enable common party accounting # enable common party accounting
frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 1) frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
# create a sales invoice # create a sales invoice
si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC") si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC")
@ -2926,7 +2925,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(jv[0], si.grand_total) self.assertEqual(jv[0], si.grand_total)
party_link.delete() party_link.delete()
frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0) frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
def test_payment_statuses(self): def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@ -2982,7 +2981,7 @@ class TestSalesInvoice(unittest.TestCase):
# Sales Invoice with Payment Schedule # Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True) si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend( si_with_payment_schedule.set(
"payment_schedule", "payment_schedule",
[ [
{ {
@ -3046,7 +3045,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, si.save) self.assertRaises(frappe.ValidationError, si.save)
def test_sales_invoice_submission_post_account_freezing_date(self): def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1)) frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True) si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1) si.posting_date = add_days(getdate(), 1)
si.save() si.save()
@ -3055,7 +3054,7 @@ class TestSalesInvoice(unittest.TestCase):
si.posting_date = getdate() si.posting_date = getdate()
si.submit() si.submit()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def test_over_billing_case_against_delivery_note(self): def test_over_billing_case_against_delivery_note(self):
""" """
@ -3067,7 +3066,7 @@ class TestSalesInvoice(unittest.TestCase):
over_billing_allowance = frappe.db.get_single_value( over_billing_allowance = frappe.db.get_single_value(
"Accounts Settings", "over_billing_allowance" "Accounts Settings", "over_billing_allowance"
) )
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
dn = create_delivery_note() dn = create_delivery_note()
dn.submit() dn.submit()
@ -3083,7 +3082,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue("cannot overbill" in str(err.exception).lower()) self.assertTrue("cannot overbill" in str(err.exception).lower())
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance) frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
def test_multi_currency_deferred_revenue_via_journal_entry(self): def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account( deferred_account = create_account(
@ -3122,7 +3121,7 @@ class TestSalesInvoice(unittest.TestCase):
si.save() si.save()
si.submit() si.submit()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", getdate("2019-01-31")) frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", getdate("2019-01-31"))
pda1 = frappe.get_doc( pda1 = frappe.get_doc(
dict( dict(
@ -3167,14 +3166,14 @@ class TestSalesInvoice(unittest.TestCase):
acc_settings.submit_journal_entries = 0 acc_settings.submit_journal_entries = 0
acc_settings.save() acc_settings.save()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def test_standalone_serial_no_return(self): def test_standalone_serial_no_return(self):
si = create_sales_invoice( si = create_sales_invoice(
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
) )
si.reload() si.reload()
self.assertTrue(si.items[0].serial_no) self.assertTrue(get_serial_nos_from_bundle(si.items[0].serial_and_batch_bundle))
def test_sales_invoice_with_disabled_account(self): def test_sales_invoice_with_disabled_account(self):
try: try:
@ -3217,9 +3216,7 @@ class TestSalesInvoice(unittest.TestCase):
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
) )
frappe.db.set_value( frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
)
jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False) jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
@ -3262,8 +3259,8 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, nowdate()) check_gl_entries(self, si.name, expected_gle, nowdate())
frappe.db.set_value( frappe.db.set_single_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
) )
def test_batch_expiry_for_sales_invoice_return(self): def test_batch_expiry_for_sales_invoice_return(self):
@ -3283,11 +3280,11 @@ class TestSalesInvoice(unittest.TestCase):
pr = make_purchase_receipt(qty=1, item_code=item.name) pr = make_purchase_receipt(qty=1, item_code=item.name)
batch_no = pr.items[0].batch_no batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no) si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
si.load_from_db() si.load_from_db()
batch_no = si.items[0].batch_no batch_no = get_batch_from_bundle(si.items[0].serial_and_batch_bundle)
self.assertTrue(batch_no) self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
@ -3445,6 +3442,33 @@ def create_sales_invoice(**args):
si.naming_series = args.naming_series or "T-SINV-" si.naming_series = args.naming_series or "T-SINV-"
si.cost_center = args.parent_cost_center si.cost_center = args.parent_cost_center
bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {}
qty = args.qty or 1
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
serial_nos = args.get("serial_no") or []
bundle_id = make_serial_batch_bundle(
frappe._dict(
{
"item_code": item_code,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": qty,
"batches": batches,
"voucher_type": "Sales Invoice",
"serial_nos": serial_nos,
"type_of_transaction": "Outward" if not args.is_return else "Inward",
"posting_date": si.posting_date or today(),
"posting_time": si.posting_time,
"do_not_submit": True,
}
)
).name
si.append( si.append(
"items", "items",
{ {
@ -3464,10 +3488,9 @@ def create_sales_invoice(**args):
"discount_amount": args.discount_amount or 0, "discount_amount": args.discount_amount or 0,
"asset": args.asset or None, "asset": args.asset or None,
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": args.get("conversion_factor", 1), "conversion_factor": args.get("conversion_factor", 1),
"incoming_rate": args.incoming_rate or 0, "incoming_rate": args.incoming_rate or 0,
"batch_no": args.batch_no or None, "serial_and_batch_bundle": bundle_id,
}, },
) )
@ -3477,6 +3500,8 @@ def create_sales_invoice(**args):
si.submit() si.submit()
else: else:
si.payment_schedule = [] si.payment_schedule = []
si.load_from_db()
else: else:
si.payment_schedule = [] si.payment_schedule = []
@ -3511,7 +3536,6 @@ def create_sales_invoice_against_cost_center(**args):
"income_account": "Sales - _TC", "income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC", "expense_account": "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
}, },
) )

View File

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

View File

@ -15,7 +15,7 @@ test_records = frappe.get_test_records("Tax Rule")
class TestTaxRule(unittest.TestCase): class TestTaxRule(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
frappe.db.set_value("Shopping Cart Settings", None, "enabled", 0) frappe.db.set_single_value("Shopping Cart Settings", "enabled", 0)
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):

View File

@ -3,9 +3,11 @@
import frappe import frappe
from frappe import _ from frappe import _, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, getdate from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, flt, getdate
class TaxWithholdingCategory(Document): class TaxWithholdingCategory(Document):
@ -346,26 +348,33 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
def get_advance_vouchers( def get_advance_vouchers(
parties, company=None, from_date=None, to_date=None, party_type="Supplier" parties, company=None, from_date=None, to_date=None, party_type="Supplier"
): ):
# for advance vouchers, debit and credit is reversed """
dr_or_cr = "debit" if party_type == "Supplier" else "credit" Use Payment Ledger to fetch unallocated Advance Payments
"""
filters = { ple = qb.DocType("Payment Ledger Entry")
dr_or_cr: [">", 0],
"is_opening": "No",
"is_cancelled": 0,
"party_type": party_type,
"party": ["in", parties],
}
if party_type == "Customer": conditions = []
filters.update({"against_voucher": ["is", "not set"]})
conditions.append(ple.amount.lt(0))
conditions.append(ple.delinked == 0)
conditions.append(ple.party_type == party_type)
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
if company: if company:
filters["company"] = company conditions.append(ple.company == company)
if from_date and to_date:
filters["posting_date"] = ["between", (from_date, to_date)]
return frappe.get_all("GL Entry", filters=filters, distinct=1, pluck="voucher_no") or [""] if from_date and to_date:
conditions.append(ple.posting_date[from_date:to_date])
advances = (
qb.from_(ple).select(ple.voucher_no).distinct().where(Criterion.all(conditions)).run(as_list=1)
)
if advances:
advances = [x[0] for x in advances]
return advances
def get_taxes_deducted_on_advances_allocated(inv, tax_details): def get_taxes_deducted_on_advances_allocated(inv, tax_details):
@ -499,6 +508,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0 tcs_amount = 0
ple = qb.DocType("Payment Ledger Entry")
# sum of debit entries made from sales invoices # sum of debit entries made from sales invoices
invoiced_amt = ( invoiced_amt = (
@ -516,18 +526,20 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
) )
# sum of credit entries made from PE / JV with unset 'against voucher' # sum of credit entries made from PE / JV with unset 'against voucher'
advance_amt = (
frappe.db.get_value( conditions = []
"GL Entry", conditions.append(ple.amount.lt(0))
{ conditions.append(ple.delinked == 0)
"is_cancelled": 0, conditions.append(ple.party.isin(parties))
"party": ["in", parties], conditions.append(ple.voucher_no == ple.against_voucher_no)
"company": inv.company, conditions.append(ple.company == inv.company)
"voucher_no": ["in", adv_vouchers],
}, advances = (
"sum(credit)", qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1)
) )
or 0.0
advance_amt = (
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
) )
# sum of credit entries made from sales invoice # sum of credit entries made from sales invoice
@ -569,7 +581,12 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
tds_amount = 0 tds_amount = 0
limit_consumed = frappe.db.get_value( limit_consumed = frappe.db.get_value(
"Purchase Invoice", "Purchase Invoice",
{"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1}, {
"supplier": ("in", parties),
"apply_tds": 1,
"docstatus": 1,
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
},
"sum(tax_withholding_net_total)", "sum(tax_withholding_net_total)",
) )
@ -584,10 +601,10 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount): if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
return current_amount * rate / 100 return current_amount * rate / 100
else: else:
ltds_amount = certificate_limit - deducted_amount ltds_amount = certificate_limit - flt(deducted_amount)
tds_amount = current_amount - ltds_amount tds_amount = current_amount - ltds_amount
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100 return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
@ -598,9 +615,9 @@ def is_valid_certificate(
): ):
valid = False valid = False
if ( available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount)
getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
) and certificate_limit > deducted_amount: if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
valid = True valid = True
return valid return valid

View File

@ -152,6 +152,60 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in reversed(invoices): for d in reversed(invoices):
d.cancel() d.cancel()
def test_tcs_on_unallocated_advance_payments(self):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
vouchers = []
# create advance payment
pe = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
)
pe.paid_from = "Debtors - _TC"
pe.paid_to = "Cash - _TC"
pe.submit()
vouchers.append(pe)
# create invoice
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
si1.submit()
vouchers.append(si1)
# reconcile
pr = frappe.get_doc("Payment Reconciliation")
pr.company = "_Test Company"
pr.party_type = "Customer"
pr.party = "Test TCS Customer"
pr.receivable_payable_account = "Debtors - _TC"
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# make another invoice
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
# TDS should be calculated
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
si2.submit()
vouchers.append(si2)
si3 = create_sales_invoice(customer="Test TCS Customer", rate=10000)
si3.submit()
vouchers.append(si3)
# assert tax collection on total invoice amount created until now
tcs_charged = sum([d.base_tax_amount for d in si2.taxes if d.account_head == "TCS - _TC"])
tcs_charged += sum([d.base_tax_amount for d in si3.taxes if d.account_head == "TCS - _TC"])
self.assertEqual(tcs_charged, 1500)
# cancel invoice and payments to avoid clashing
for d in reversed(vouchers):
d.reload()
d.cancel()
def test_tds_calculation_on_net_total(self): def test_tds_calculation_on_net_total(self):
frappe.db.set_value( frappe.db.set_value(
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS" "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"

View File

@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from typing import Optional
import frappe import frappe
from frappe import _, msgprint, scrub from frappe import _, msgprint, scrub
from frappe.contacts.doctype.address.address import ( from frappe.contacts.doctype.address.address import (
@ -680,12 +682,12 @@ def set_taxes(
else: else:
args.update(get_party_details(party, party_type)) args.update(get_party_details(party, party_type))
if party_type in ("Customer", "Lead"): if party_type in ("Customer", "Lead", "Prospect"):
args.update({"tax_type": "Sales"}) args.update({"tax_type": "Sales"})
if party_type == "Lead": if party_type in ["Lead", "Prospect"]:
args["customer"] = None args["customer"] = None
del args["lead"] del args[frappe.scrub(party_type)]
else: else:
args.update({"tax_type": "Purchase"}) args.update({"tax_type": "Purchase"})
@ -883,7 +885,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
return company_wise_info return company_wise_info
def get_party_shipping_address(doctype, name): def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
""" """
Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true. Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true.
and/or `is_shipping_address = 1`. and/or `is_shipping_address = 1`.
@ -894,22 +896,23 @@ def get_party_shipping_address(doctype, name):
:param name: Party name :param name: Party name
:return: String :return: String
""" """
out = frappe.db.sql( shipping_addresses = frappe.get_all(
"SELECT dl.parent " "Address",
"from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name " filters=[
"where " ["Dynamic Link", "link_doctype", "=", doctype],
"dl.link_doctype=%s " ["Dynamic Link", "link_name", "=", name],
"and dl.link_name=%s " ["disabled", "=", 0],
"and dl.parenttype='Address' " ],
"and ifnull(ta.disabled, 0) = 0 and" or_filters=[
"(ta.address_type='Shipping' or ta.is_shipping_address=1) " ["is_shipping_address", "=", 1],
"order by ta.is_shipping_address desc, ta.address_type desc limit 1", ["address_type", "=", "Shipping"],
(doctype, name), ],
pluck="name",
limit=1,
order_by="is_shipping_address DESC",
) )
if out:
return out[0][0] return shipping_addresses[0] if shipping_addresses else None
else:
return ""
def get_partywise_advanced_payment_amount( def get_partywise_advanced_payment_amount(
@ -943,31 +946,32 @@ def get_partywise_advanced_payment_amount(
return frappe._dict(data) return frappe._dict(data)
def get_default_contact(doctype, name): def get_default_contact(doctype: str, name: str) -> Optional[str]:
""" """
Returns default contact for the given doctype and name. Returns contact name only if there is a primary contact for given doctype and name.
Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
Else returns None
:param doctype: Party Doctype
:param name: Party name
:return: String
""" """
out = frappe.db.sql( contacts = frappe.get_all(
""" "Contact",
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact filters=[
FROM `tabDynamic Link` dl ["Dynamic Link", "link_doctype", "=", doctype],
INNER JOIN `tabContact` c ON c.name = dl.parent ["Dynamic Link", "link_name", "=", name],
WHERE ],
dl.link_doctype=%s AND or_filters=[
dl.link_name=%s AND ["is_primary_contact", "=", 1],
dl.parenttype = 'Contact' ["is_billing_contact", "=", 1],
ORDER BY is_primary_contact DESC, is_billing_contact DESC ],
""", pluck="name",
(doctype, name), limit=1,
order_by="is_primary_contact DESC, is_billing_contact DESC",
) )
if out:
try: return contacts[0] if contacts else None
return out[0][0]
except Exception:
return None
else:
return None
def add_party_account(party_type, party, company, account): def add_party_account(party_type, party, company, account):

View File

@ -181,6 +181,16 @@ class ReceivablePayableReport(object):
return return
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
# If payment is made against credit note
# and credit note is made against a Sales Invoice
# then consider the payment against original sales invoice.
if ple.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
if ple.against_voucher_no in self.return_entries:
return_against = self.return_entries.get(ple.against_voucher_no)
if return_against:
key = (ple.against_voucher_type, return_against, ple.party)
row = self.voucher_balance.get(key) row = self.voucher_balance.get(key)
if not row: if not row:
@ -610,7 +620,7 @@ class ReceivablePayableReport(object):
def get_return_entries(self): def get_return_entries(self):
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
filters = {"is_return": 1, "docstatus": 1} filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
party_field = scrub(self.filters.party_type) party_field = scrub(self.filters.party_type)
if self.filters.get(party_field): if self.filters.get(party_field):
filters.update({party_field: self.filters.get(party_field)}) filters.update({party_field: self.filters.get(party_field)})

View File

@ -210,6 +210,67 @@ class TestAccountsReceivable(FrappeTestCase):
], ],
) )
def test_payment_against_credit_note(self):
"""
Payment against credit/debit note should be considered against the parent invoice
"""
company = "_Test Company 2"
customer = "_Test Customer 2"
si1 = make_sales_invoice()
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
pe.paid_from = "Debtors - _TC2"
pe.insert()
pe.submit()
cr_note = make_credit_note(si1.name)
si2 = make_sales_invoice()
# manually link cr_note with si2 using journal entry
je = frappe.new_doc("Journal Entry")
je.company = company
je.voucher_type = "Credit Note"
je.posting_date = today()
debit_account = "Debtors - _TC2"
debit_entry = {
"account": debit_account,
"party_type": "Customer",
"party": customer,
"debit": 100,
"debit_in_account_currency": 100,
"reference_type": cr_note.doctype,
"reference_name": cr_note.name,
"cost_center": "Main - _TC2",
}
credit_entry = {
"account": debit_account,
"party_type": "Customer",
"party": customer,
"credit": 100,
"credit_in_account_currency": 100,
"reference_type": si2.doctype,
"reference_name": si2.name,
"cost_center": "Main - _TC2",
}
je.append("accounts", debit_entry)
je.append("accounts", credit_entry)
je = je.save().submit()
filters = {
"company": company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report = execute(filters)
self.assertEqual(report[1], [])
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator") frappe.set_user("Administrator")
@ -256,7 +317,7 @@ def make_payment(docname):
def make_credit_note(docname): def make_credit_note(docname):
create_sales_invoice( credit_note = create_sales_invoice(
company="_Test Company 2", company="_Test Company 2",
customer="_Test Customer 2", customer="_Test Customer 2",
currency="EUR", currency="EUR",
@ -269,3 +330,5 @@ def make_credit_note(docname):
is_return=1, is_return=1,
return_against=docname, return_against=docname,
) )
return credit_note

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from collections import OrderedDict
import frappe import frappe
from frappe import _, qb, scrub from frappe import _, qb, scrub
@ -702,6 +703,9 @@ class GrossProfitGenerator(object):
} }
) )
if row.serial_and_batch_bundle:
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
average_buying_rate = get_incoming_rate(args) average_buying_rate = get_incoming_rate(args)
self.average_buying_rate[item_code] = flt(average_buying_rate) self.average_buying_rate[item_code] = flt(average_buying_rate)
@ -804,7 +808,7 @@ class GrossProfitGenerator(object):
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
`tabSales Invoice Item`.cost_center `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
{sales_person_cols} {sales_person_cols}
{payment_term_cols} {payment_term_cols}
from from
@ -856,30 +860,30 @@ class GrossProfitGenerator(object):
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
""" """
parents = [] grouped = OrderedDict()
for row in self.si_list: for row in self.si_list:
if row.parent not in parents: # initialize list with a header row for each new parent
parents.append(row.parent) grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append(
row.update(
parents_index = 0 {"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code}
for index, row in enumerate(self.si_list): ) # descendant rows will have indent: 1.0 or greater
if parents_index < len(parents) and row.parent == parents[parents_index]: )
invoice = self.get_invoice_row(row)
self.si_list.insert(index, invoice)
parents_index += 1
else:
# skipping the bundle items rows
if not row.indent:
row.indent = 1.0
row.parent_invoice = row.parent
row.invoice_or_item = row.item_code
# if item is a bundle, add it's components as seperate rows
if frappe.db.exists("Product Bundle", row.item_code): if frappe.db.exists("Product Bundle", row.item_code):
self.add_bundle_items(row, index) bundled_items = self.get_bundle_items(row)
for x in bundled_items:
bundle_item = self.get_bundle_item_row(row, x)
grouped.get(row.parent).append(bundle_item)
self.si_list.clear()
for items in grouped.values():
self.si_list.extend(items)
def get_invoice_row(self, row): def get_invoice_row(self, row):
# header row format
return frappe._dict( return frappe._dict(
{ {
"parent_invoice": "", "parent_invoice": "",
@ -908,13 +912,6 @@ class GrossProfitGenerator(object):
} }
) )
def add_bundle_items(self, product_bundle, index):
bundle_items = self.get_bundle_items(product_bundle)
for i, item in enumerate(bundle_items):
bundle_item = self.get_bundle_item_row(product_bundle, item)
self.si_list.insert((index + i + 1), bundle_item)
def get_bundle_items(self, product_bundle): def get_bundle_items(self, product_bundle):
return frappe.get_all( return frappe.get_all(
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]

View File

@ -399,8 +399,9 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.is_internal_customer, `tabSales Invoice`.is_internal_customer,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.project,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`, `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,

View File

@ -5,8 +5,9 @@
"label": "Profit and Loss" "label": "Profit and Loss"
} }
], ],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}}]", "content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 15:41:59.515192", "creation": "2020-03-02 15:41:59.515192",
"custom_blocks": [],
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
"for_user": "", "for_user": "",
@ -1060,10 +1061,11 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2023-02-23 15:32:12.135355", "modified": "2023-05-30 13:23:29.316711",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",
"number_cards": [],
"owner": "Administrator", "owner": "Administrator",
"parent_page": "", "parent_page": "",
"public": 1, "public": 1,

View File

@ -41,6 +41,8 @@ frappe.ui.form.on('Asset', {
}, },
setup: function(frm) { setup: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Journal Entry'];
frm.make_methods = { frm.make_methods = {
'Asset Movement': () => { 'Asset Movement': () => {
frappe.call({ frappe.call({

View File

@ -513,6 +513,10 @@ def get_gl_entries_on_asset_disposal(
}, },
item=asset, item=asset,
), ),
]
if accumulated_depr_amount:
gl_entries.append(
asset.get_gl_dict( asset.get_gl_dict(
{ {
"account": accumulated_depr_account, "account": accumulated_depr_account,
@ -523,7 +527,7 @@ def get_gl_entries_on_asset_disposal(
}, },
item=asset, item=asset,
), ),
] )
profit_amount = flt(selling_amount) - flt(value_after_depreciation) profit_amount = flt(selling_amount) - flt(value_after_depreciation)
if profit_amount: if profit_amount:

View File

@ -812,14 +812,14 @@ class TestDepreciationMethods(AssetSetup):
number_of_depreciations_booked=1, number_of_depreciations_booked=1,
opening_accumulated_depreciation=50000, opening_accumulated_depreciation=50000,
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
depreciation_start_date="2030-12-31", depreciation_start_date="2031-12-31",
total_number_of_depreciations=3, total_number_of_depreciations=3,
frequency_of_depreciation=12, frequency_of_depreciation=12,
) )
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]] expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
schedules = [ schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@ -1804,7 +1804,7 @@ def set_depreciation_settings_in_company(company=None):
company.save() company.save()
# Enable booking asset depreciation entry automatically # Enable booking asset depreciation entry automatically
frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) frappe.db.set_single_value("Accounts Settings", "book_asset_depreciation_entry_automatically", 1)
def enable_cwip_accounting(asset_category, enable=1): def enable_cwip_accounting(asset_category, enable=1):

View File

@ -6,6 +6,7 @@ frappe.provide("erpnext.assets");
erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController {
setup() { setup() {
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
} }
@ -64,6 +65,18 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
}; };
}); });
me.frm.set_query("serial_and_batch_bundle", "stock_items", (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
'item_code': row.item_code,
'voucher_type': doc.doctype,
'voucher_no': ["in", [doc.name, ""]],
'is_cancelled': 0,
}
}
});
me.frm.set_query("item_code", "stock_items", function() { me.frm.set_query("item_code", "stock_items", function() {
return erpnext.queries.item({"is_stock_item": 1}); return erpnext.queries.item({"is_stock_item": 1});
}); });
@ -99,6 +112,17 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
} }
}; };
}); });
let sbb_field = me.frm.get_docfield('stock_items', 'serial_and_batch_bundle');
if (sbb_field) {
sbb_field.get_route_options_for_new_doc = (row) => {
return {
'item_code': row.doc.item_code,
'warehouse': row.doc.warehouse,
'voucher_type': me.frm.doc.doctype,
}
};
}
} }
target_item_code() { target_item_code() {

View File

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

View File

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

View File

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

View File

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

View File

@ -96,7 +96,6 @@ class AssetCategory(Document):
frappe.throw(msg, title=_("Missing Account")) frappe.throw(msg, title=_("Missing Account"))
@frappe.whitelist()
def get_asset_category_account( def get_asset_category_account(
fieldname, item=None, asset=None, account=None, asset_category=None, company=None fieldname, item=None, asset=None, account=None, asset_category=None, company=None
): ):

View File

@ -10,6 +10,7 @@ from frappe.utils import (
cint, cint,
date_diff, date_diff,
flt, flt,
get_first_day,
get_last_day, get_last_day,
getdate, getdate,
is_last_day_of_the_month, is_last_day_of_the_month,
@ -246,10 +247,6 @@ class AssetDepreciationSchedule(Document):
if should_get_last_day: if should_get_last_day:
schedule_date = get_last_day(schedule_date) schedule_date = get_last_day(schedule_date)
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1)
# if asset is being sold or scrapped # if asset is being sold or scrapped
if date_of_disposal: if date_of_disposal:
from_date = add_months( from_date = add_months(
@ -276,9 +273,9 @@ class AssetDepreciationSchedule(Document):
# For first row # For first row
if ( if (
(has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation and not self.opening_accumulated_depreciation
and n == 0
): ):
from_date = add_days( from_date = add_days(
asset_doc.available_for_use_date, -1 asset_doc.available_for_use_date, -1
@ -290,11 +287,26 @@ class AssetDepreciationSchedule(Document):
row.depreciation_start_date, row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
) )
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
# For first depr schedule date will be the start date if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
# so monthly schedule date is calculated by removing from_date = get_last_day(
# month difference between use date and start date add_months(
monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1) getdate(asset_doc.available_for_use_date),
((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation),
)
)
else:
from_date = add_months(
getdate(add_days(asset_doc.available_for_use_date, -1)),
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
)
depreciation_amount, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
from_date,
row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata,
)
# For last row # For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
@ -319,9 +331,7 @@ class AssetDepreciationSchedule(Document):
depreciation_amount_without_pro_rata, depreciation_amount depreciation_amount_without_pro_rata, depreciation_amount
) )
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days) schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount: if not depreciation_amount:
continue continue
@ -707,3 +717,9 @@ def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
["status", "=", status], ["status", "=", status],
], ],
) )
def is_first_day_of_the_month(date):
first_day_of_the_month = get_first_day(date)
return getdate(first_day_of_the_month) == getdate(date)

View File

@ -182,4 +182,4 @@ def set_depreciation_settings_in_company():
company.save() company.save()
# Enable booking asset depreciation entry automatically # Enable booking asset depreciation entry automatically
frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) frappe.db.set_single_value("Accounts Settings", "book_asset_depreciation_entry_automatically", 1)

View File

@ -28,6 +28,28 @@ frappe.ui.form.on('Asset Repair', {
} }
}; };
}; };
frm.set_query("serial_and_batch_bundle", "stock_items", (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
'item_code': row.item_code,
'voucher_type': doc.doctype,
'voucher_no': ["in", [doc.name, ""]],
'is_cancelled': 0,
}
}
});
let sbb_field = frm.get_docfield('stock_items', 'serial_and_batch_bundle');
if (sbb_field) {
sbb_field.get_route_options_for_new_doc = (row) => {
return {
'item_code': row.doc.item_code,
'voucher_type': frm.doc.doctype,
}
};
}
}, },
refresh: function(frm) { refresh: function(frm) {

View File

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

View File

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

View File

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

View File

@ -7,12 +7,14 @@
], ],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Assets\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset Category\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fixed Asset Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Assets\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset Category\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fixed Asset Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:43:27.634865", "creation": "2020-03-02 15:43:27.634865",
"custom_blocks": [],
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
"for_user": "", "for_user": "",
"hide_custom": 0, "hide_custom": 0,
"icon": "assets", "icon": "assets",
"idx": 0, "idx": 0,
"is_hidden": 0,
"label": "Assets", "label": "Assets",
"links": [ "links": [
{ {
@ -183,13 +185,15 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2022-01-13 18:25:41.730628", "modified": "2023-05-24 14:47:20.243146",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Assets", "name": "Assets",
"number_cards": [],
"owner": "Administrator", "owner": "Administrator",
"parent_page": "", "parent_page": "Accounting",
"public": 1, "public": 1,
"quick_lists": [],
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 4.0, "sequence_id": 4.0,

View File

@ -322,6 +322,7 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"label": "Customer Mobile No", "label": "Customer Mobile No",
"options": "Phone",
"print_hide": 1 "print_hide": 1
}, },
{ {
@ -368,6 +369,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact Mobile No", "label": "Contact Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1271,7 +1273,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-05-24 11:16:41.195340", "modified": "2023-06-03 16:19:45.710444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -92,7 +92,7 @@ class TestPurchaseOrder(FrappeTestCase):
frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 0) frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 0)
frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 0) frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 0)
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
def test_update_remove_child_linked_to_mr(self): def test_update_remove_child_linked_to_mr(self):
"""Test impact on linked PO and MR on deleting/updating row.""" """Test impact on linked PO and MR on deleting/updating row."""
@ -581,7 +581,7 @@ class TestPurchaseOrder(FrappeTestCase):
) )
def test_group_same_items(self): def test_group_same_items(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1)
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Purchase Order", "doctype": "Purchase Order",
@ -836,8 +836,8 @@ class TestPurchaseOrder(FrappeTestCase):
) )
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
frappe.db.set_value("Selling Settings", None, "maintain_same_sales_rate", 1) frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1)
frappe.db.set_value("Buying Settings", None, "maintain_same_rate", 1) frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
prepare_data_for_internal_transfer() prepare_data_for_internal_transfer()
supplier = "_Test Internal Supplier 2" supplier = "_Test Internal Supplier 2"

View File

@ -156,7 +156,7 @@ class TestSupplier(FrappeTestCase):
def test_serach_fields_for_supplier(self): def test_serach_fields_for_supplier(self):
from erpnext.controllers.queries import supplier_query from erpnext.controllers.queries import supplier_query
frappe.db.set_value("Buying Settings", None, "supp_master_name", "Naming Series") frappe.db.set_single_value("Buying Settings", "supp_master_name", "Naming Series")
supplier_name = create_supplier(supplier_name="Test Supplier 1").name supplier_name = create_supplier(supplier_name="Test Supplier 1").name
@ -189,7 +189,7 @@ class TestSupplier(FrappeTestCase):
self.assertEqual(data[0].supplier_type, "Company") self.assertEqual(data[0].supplier_type, "Company")
self.assertTrue("supplier_type" in data[0]) self.assertTrue("supplier_type" in data[0])
frappe.db.set_value("Buying Settings", None, "supp_master_name", "Supplier Name") frappe.db.set_single_value("Buying Settings", "supp_master_name", "Supplier Name")
def create_supplier(**args): def create_supplier(**args):

View File

@ -230,6 +230,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -844,7 +845,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-14 16:43:41.714832", "modified": "2023-06-03 16:20:15.880114",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@ -7,12 +7,14 @@
], ],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Buying\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]", "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Buying\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]",
"creation": "2020-01-28 11:50:26.195467", "creation": "2020-01-28 11:50:26.195467",
"custom_blocks": [],
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
"for_user": "", "for_user": "",
"hide_custom": 0, "hide_custom": 0,
"icon": "buying", "icon": "buying",
"idx": 0, "idx": 0,
"is_hidden": 0,
"label": "Buying", "label": "Buying",
"links": [ "links": [
{ {
@ -509,16 +511,18 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2022-01-13 17:26:39.090190", "modified": "2023-05-24 14:47:20.535772",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying", "name": "Buying",
"number_cards": [],
"owner": "Administrator", "owner": "Administrator",
"parent_page": "", "parent_page": "",
"public": 1, "public": 1,
"quick_lists": [],
"restrict_to_domain": "", "restrict_to_domain": "",
"roles": [], "roles": [],
"sequence_id": 6.0, "sequence_id": 5.0,
"shortcuts": [ "shortcuts": [
{ {
"color": "Green", "color": "Green",

View File

@ -759,6 +759,7 @@ class AccountsController(TransactionBase):
} }
) )
update_gl_dict_with_regional_fields(self, gl_dict)
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
dimension_dict = frappe._dict() dimension_dict = frappe._dict()
@ -921,6 +922,9 @@ class AccountsController(TransactionBase):
return is_inclusive return is_inclusive
def should_show_taxes_as_table_in_print(self):
return cint(frappe.db.get_single_value("Accounts Settings", "show_taxes_as_table_in_print"))
def validate_advance_entries(self): def validate_advance_entries(self):
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order" order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field))) order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
@ -2514,7 +2518,7 @@ def set_order_defaults(
Returns a Sales/Purchase Order Item child item containing the default values Returns a Sales/Purchase Order Item child item containing the default values
""" """
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc(child_doctype, p_doc, child_docname) child_item = frappe.new_doc(child_doctype, parent_doc=p_doc, parentfield=child_docname)
item = frappe.get_doc("Item", trans_item.get("item_code")) item = frappe.get_doc("Item", trans_item.get("item_code"))
for field in ("item_code", "item_name", "description", "item_group"): for field in ("item_code", "item_name", "description", "item_group"):

View File

@ -5,7 +5,7 @@
import frappe import frappe
from frappe import ValidationError, _, msgprint from frappe import ValidationError, _, msgprint
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, getdate from frappe.utils import cint, flt, getdate
from frappe.utils.data import nowtime from frappe.utils.data import nowtime
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
@ -26,6 +26,8 @@ class BuyingController(SubcontractingController):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
def validate(self): def validate(self):
self.set_rate_for_standalone_debit_note()
super(BuyingController, self).validate() super(BuyingController, self).validate()
if getattr(self, "supplier", None) and not self.supplier_name: if getattr(self, "supplier", None) and not self.supplier_name:
self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name") self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name")
@ -38,6 +40,7 @@ class BuyingController(SubcontractingController):
self.set_supplier_address() self.set_supplier_address()
self.validate_asset_return() self.validate_asset_return()
self.validate_auto_repeat_subscription_dates() self.validate_auto_repeat_subscription_dates()
self.create_package_for_transfer()
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
self.validate_purchase_receipt_if_update_stock() self.validate_purchase_receipt_if_update_stock()
@ -58,6 +61,7 @@ class BuyingController(SubcontractingController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate() self.update_valuation_rate()
self.set_serial_and_batch_bundle()
def onload(self): def onload(self):
super(BuyingController, self).onload() super(BuyingController, self).onload()
@ -68,6 +72,60 @@ 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_rate_for_standalone_debit_note(self):
if self.get("is_return") and self.get("update_stock") and not self.return_against:
for row in self.items:
# override the rate with valuation rate
row.rate = get_incoming_rate(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.get("posting_date"),
"posting_time": self.get("posting_time"),
"qty": row.qty,
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
},
raise_error_if_no_rate=False,
)
row.discount_percentage = 0.0
row.discount_amount = 0.0
row.margin_rate_or_amount = 0.0
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@ -180,6 +238,7 @@ class BuyingController(SubcontractingController):
address_dict = { address_dict = {
"supplier_address": "address_display", "supplier_address": "address_display",
"shipping_address": "shipping_address_display", "shipping_address": "shipping_address_display",
"billing_address": "billing_address_display",
} }
for address_field, address_display_field in address_dict.items(): for address_field, address_display_field in address_dict.items():
@ -304,8 +363,7 @@ class BuyingController(SubcontractingController):
"posting_date": self.get("posting_date") or self.get("transation_date"), "posting_date": self.get("posting_date") or self.get("transation_date"),
"posting_time": posting_time, "posting_time": posting_time,
"qty": -1 * flt(d.get("stock_qty")), "qty": -1 * flt(d.get("stock_qty")),
"serial_no": d.get("serial_no"), "serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"batch_no": d.get("batch_no"),
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
@ -440,7 +498,7 @@ class BuyingController(SubcontractingController):
continue continue
if d.warehouse: if d.warehouse:
pr_qty = flt(d.qty) * flt(d.conversion_factor) pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
if pr_qty: if pr_qty:
@ -462,7 +520,15 @@ class BuyingController(SubcontractingController):
sl_entries.append(from_warehouse_sle) sl_entries.append(from_warehouse_sle)
sle = self.get_sl_entries( sle = self.get_sl_entries(
d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()} d,
{
"actual_qty": flt(pr_qty),
"serial_and_batch_bundle": (
d.serial_and_batch_bundle
if not self.is_internal_transfer()
else self.get_package_for_target_warehouse(d)
),
},
) )
if self.is_return: if self.is_return:
@ -470,7 +536,13 @@ class BuyingController(SubcontractingController):
self.doctype, self.name, d.item_code, self.return_against, item_row=d self.doctype, self.name, d.item_code, self.return_against, item_row=d
) )
sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1}) sle.update(
{
"outgoing_rate": outgoing_rate,
"recalculate_rate": 1,
"serial_and_batch_bundle": d.serial_and_batch_bundle,
}
)
if d.from_warehouse: if d.from_warehouse:
sle.dependant_sle_voucher_detail_no = d.name sle.dependant_sle_voucher_detail_no = d.name
else: else:
@ -502,21 +574,31 @@ class BuyingController(SubcontractingController):
d, d,
{ {
"warehouse": d.rejected_warehouse, "warehouse": d.rejected_warehouse,
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), "actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")),
"serial_no": cstr(d.rejected_serial_no).strip(),
"incoming_rate": 0.0, "incoming_rate": 0.0,
"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
}, },
) )
) )
if self.get("is_old_subcontracting_flow"): if self.get("is_old_subcontracting_flow"):
self.make_sl_entries_for_supplier_warehouse(sl_entries) self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries( self.make_sl_entries(
sl_entries, sl_entries,
allow_negative_stock=allow_negative_stock, allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher, via_landed_cost_voucher=via_landed_cost_voucher,
) )
def get_package_for_target_warehouse(self, item) -> str:
if not item.serial_and_batch_bundle:
return ""
return self.make_package_for_transfer(
item.serial_and_batch_bundle,
item.warehouse,
)
def update_ordered_and_reserved_qty(self): def update_ordered_and_reserved_qty(self):
po_map = {} po_map = {}
for d in self.get("items"): for d in self.get("items"):

View File

@ -30,6 +30,12 @@ def set_print_templates_for_taxes(doc, settings):
doc.print_templates.update( doc.print_templates.update(
{ {
"total": "templates/print_formats/includes/total.html", "total": "templates/print_formats/includes/total.html",
}
)
if not doc.should_show_taxes_as_table_in_print():
doc.print_templates.update(
{
"taxes": "templates/print_formats/includes/taxes.html", "taxes": "templates/print_formats/includes/taxes.html",
} }
) )

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