Merge pull request #30951 from frappe/version-13-hotfix

chore: Pre release for version-13
This commit is contained in:
Deepesh Garg
2022-05-10 14:42:16 +05:30
committed by GitHub
29 changed files with 619 additions and 256 deletions

View File

@@ -118,6 +118,7 @@ class BankClearance(Document):
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))

View File

@@ -467,6 +467,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.payment_account == bank_account)
)

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
// For license information, please see license.txt
frappe.provide("erpnext.accounts");
@@ -38,7 +38,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
]
};
});
this.frm.set_query("cost_center", () => {
return {
"filters": {

View File

@@ -12,7 +12,10 @@ def get_data():
"Sales Invoice": "return_against",
"Auto Repeat": "reference_document",
},
"internal_links": {"Sales Order": ["items", "sales_order"]},
"internal_links": {
"Sales Order": ["items", "sales_order"],
"Timesheet": ["timesheets", "time_sheet"],
},
"transactions": [
{
"label": _("Payment"),

View File

@@ -2585,6 +2585,7 @@ class TestSalesInvoice(unittest.TestCase):
# reset
einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 0
einvoice_settings.save()
frappe.flags.country = country
def test_einvoice_json(self):

View File

@@ -203,7 +203,7 @@ def get_loan_entries(filters):
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
entries = (
query = (
frappe.qb.from_(loan_doc)
.select(
ConstantColumn(doctype).as_("payment_document"),
@@ -217,9 +217,12 @@ def get_loan_entries(filters):
.where(account == filters.get("account"))
.where(posting_date <= getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
.run(as_dict=1)
)
if doctype == "Loan Repayment":
query.where(loan_doc.repay_from_salary == 0)
entries = query.run(as_dict=1)
loan_docs.extend(entries)
return loan_docs

View File

@@ -478,7 +478,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
gle_map[group_by_value].entries.append(gle)
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
keylist = [
gle.get("voucher_type"),
gle.get("voucher_no"),
gle.get("account"),
gle.get("party_type"),
gle.get("party"),
]
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))

View File

@@ -41,40 +41,46 @@ class AssetRepair(AccountsController):
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.increase_asset_value()
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.modify_depreciation_schedule()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.modify_depreciation_schedule()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
def before_cancel(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.decrease_asset_value()
if self.get("stock_consumption"):
self.increase_stock_quantity()
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.revert_depreciation_schedule_on_cancellation()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
if self.get("stock_consumption"):
self.increase_stock_quantity()
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.revert_depreciation_schedule_on_cancellation()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
def check_repair_status(self):
if self.repair_status == "Pending":

View File

@@ -34,15 +34,6 @@ frappe.ui.form.on("Leave Allocation", {
});
}
}
// make new leaves allocated field read only if allocation is created via leave policy assignment
// and leave type is earned leave, since these leaves would be allocated via the scheduler
if (frm.doc.leave_policy_assignment) {
frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
if (r && cint(r.is_earned_leave))
frm.set_df_property("new_leaves_allocated", "read_only", 1);
});
}
},
expire_allocation: function(frm) {

View File

@@ -254,7 +254,18 @@ class LeaveAllocation(Document):
# Adding a day to include To Date in the difference
date_difference = date_diff(self.to_date, self.from_date) + 1
if date_difference < self.total_leaves_allocated:
frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
if frappe.db.get_value("Leave Type", self.leave_type, "allow_over_allocation"):
frappe.msgprint(
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
indicator="orange",
alert=True,
)
else:
frappe.throw(
_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"),
exc=OverAllocationError,
title=_("Over Allocation"),
)
def create_leave_ledger_entry(self, submit=True):
if self.unused_leaves:

View File

@@ -68,22 +68,44 @@ class TestLeaveAllocation(FrappeTestCase):
self.assertRaises(frappe.ValidationError, doc.save)
def test_validation_for_over_allocation(self):
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
leave_type.save()
doc = frappe.get_doc(
{
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"leave_type": leave_type.name,
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35,
"carry_forward": 1,
}
)
# allocated leave more than period
self.assertRaises(OverAllocationError, doc.save)
leave_type.allow_over_allocation = 1
leave_type.save()
# allows creating a leave allocation with more leave days than period days
doc = frappe.get_doc(
{
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": leave_type.name,
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35,
"carry_forward": 1,
}
).insert()
def test_validation_for_over_allocation_post_submission(self):
allocation = frappe.get_doc(
{

View File

@@ -744,7 +744,7 @@ class TestLeaveApplication(unittest.TestCase):
i = 0
while i < 14:
allocate_earned_leaves(ignore_duplicates=True)
allocate_earned_leaves()
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
@@ -752,7 +752,7 @@ class TestLeaveApplication(unittest.TestCase):
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
i = 0
while i < 6:
allocate_earned_leaves(ignore_duplicates=True)
allocate_earned_leaves()
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import (
@@ -18,7 +19,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
test_dependencies = ["Employee"]
class TestLeavePolicyAssignment(unittest.TestCase):
class TestLeavePolicyAssignment(FrappeTestCase):
def setUp(self):
for doctype in [
"Leave Period",
@@ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase):
leave_policy = create_leave_policy()
leave_policy.submit()
self.employee.date_of_joining = get_first_day(leave_period.from_date)
self.employee.save()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
@@ -187,19 +191,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
)
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
@@ -241,20 +232,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(details.unused_leaves, 5)
self.assertEqual(details.total_leaves_allocated, 7)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import is_earned_leave_already_allocated
frappe.flags.current_date = get_last_day(getdate())
allocation = frappe.get_doc("Leave Allocation", details.name)
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
self.assertFalse(
is_earned_leave_already_allocated(
allocation, leave_policy.leave_policy_details[0].annual_allocation
)
)
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
leave_type = create_earned_leave_type("Test Earned Leave")
@@ -287,19 +264,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_last_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
leave_period, leave_policy = setup_leave_period_and_policy(
@@ -329,20 +293,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
)
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
@@ -376,21 +326,7 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value(
"Leave Allocation",
{"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated",
)
self.assertEqual(leaves_allocated, 3)
def tearDown(self):
frappe.db.rollback()
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
frappe.flags.current_date = None

View File

@@ -19,6 +19,7 @@
"fraction_of_daily_salary_per_leave",
"is_optional_leave",
"allow_negative",
"allow_over_allocation",
"include_holiday",
"is_compensatory",
"carry_forward_section",
@@ -211,15 +212,23 @@
"fieldtype": "Float",
"label": "Fraction of Daily Salary per Leave",
"mandatory_depends_on": "eval:doc.is_ppl == 1"
},
{
"default": "0",
"description": "Allows allocating more leaves than the number of days in the allocation period.",
"fieldname": "allow_over_allocation",
"fieldtype": "Check",
"label": "Allow Over Allocation"
}
],
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2021-10-02 11:59:40.503359",
"modified": "2022-05-09 05:01:38.957545",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -251,5 +260,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -428,7 +428,7 @@ def generate_leave_encashment():
create_leave_encashment(leave_allocation=leave_allocation)
def allocate_earned_leaves(ignore_duplicates=False):
def allocate_earned_leaves():
"""Allocate earned leaves to Employees"""
e_leave_types = get_earned_leaves()
today = getdate()
@@ -464,14 +464,10 @@ def allocate_earned_leaves(ignore_duplicates=False):
if check_effective_date(
from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
):
update_previous_leave_allocation(
allocation, annual_allocation, e_leave_type, ignore_duplicates
)
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
def update_previous_leave_allocation(
allocation, annual_allocation, e_leave_type, ignore_duplicates=False
):
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
earned_leaves = get_monthly_earned_leave(
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
)
@@ -485,20 +481,19 @@ def update_previous_leave_allocation(
if new_allocation != allocation.total_leaves_allocated:
today_date = today()
if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
if e_leave_type.based_on_date_of_joining:
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
else:
text = _("allocated {0} leave(s) via scheduler on {1}").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
if e_leave_type.based_on_date_of_joining:
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
else:
text = _("allocated {0} leave(s) via scheduler on {1}").format(
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
)
allocation.add_comment(comment_type="Info", text=text)
allocation.add_comment(comment_type="Info", text=text)
def get_monthly_earned_leave(annual_leaves, frequency, rounding):

View File

@@ -109,7 +109,7 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Membership Settings",
"label": "Non Profit Settings",
"link_to": "Non Profit Settings",
"link_type": "DocType",
"onboard": 0,
@@ -213,7 +213,7 @@
"type": "Link"
}
],
"modified": "2021-03-11 11:38:09.140655",
"modified": "2022-05-09 11:38:09.140655",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",

View File

@@ -361,5 +361,6 @@ erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v13_0.set_available_for_use_date_if_missing
erpnext.patches.v13_0.education_deprecation_warning
erpnext.patches.v13_0.create_accounting_dimensions_in_orders

View File

@@ -3,11 +3,11 @@ from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.table_exists("Membership Settings"):
if frappe.db.exists("DocType", "Membership Settings"):
frappe.rename_doc("DocType", "Membership Settings", "Non Profit Settings")
frappe.reload_doctype("Non Profit Settings", force=True)
if frappe.db.table_exists("Non Profit Settings"):
if frappe.db.exists("DocType", "Non Profit Settings"):
rename_fields_map = {
"enable_invoicing": "allow_invoicing",
"create_for_web_forms": "automate_membership_invoicing",
@@ -20,3 +20,5 @@ def execute():
for old_name, new_name in rename_fields_map.items():
rename_field("Non Profit Settings", old_name, new_name)
frappe.delete_doc_if_exists("DocType", "Membership Settings")

View File

@@ -0,0 +1,19 @@
import frappe
def execute():
"""
Sets available-for-use date for Assets created in older versions of ERPNext,
before the field was introduced.
"""
assets = get_assets_without_available_for_use_date()
for asset in assets:
frappe.db.set_value("Asset", asset.name, "available_for_use_date", asset.purchase_date)
def get_assets_without_available_for_use_date():
return frappe.get_all(
"Asset", filters={"available_for_use_date": ["in", ["", None]]}, fields=["name", "purchase_date"]
)

View File

@@ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => {
...data
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
callback: () => {
frappe.show_alert({
message: __('E-Way Bill Generated successfully'),
indicator: 'green'
}, 7);
frm.reload_doc();
d.hide();
},
error: () => {
frappe.show_alert({
message: __('E-Way Bill was not Generated'),
indicator: 'red'
}, 7);
d.hide();
}
});
},
primary_action_label: __('Submit')
@@ -136,29 +149,83 @@ erpnext.setup_einvoice_actions = (doctype) => {
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const action = () => {
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
message += '<br><br>';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
const d = new frappe.ui.Dialog({
title: __('Cancel E-Way Bill'),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: {
doctype,
docname: name,
eway_bill: ewaybill,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
callback: () => {
frappe.show_alert({
message: __('E-Way Bill Cancelled successfully'),
indicator: 'green'
}, 7);
frm.reload_doc();
d.hide();
},
error: () => {
frappe.show_alert({
message: __('E-Way Bill was not Cancelled'),
indicator: 'red'
}, 7);
d.hide();
}
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
if (irn && !irn_cancelled) {
const action = () => {
const dialog = frappe.msgprint({
title: __('Update E-Way Bill Cancelled Status?'),
message: message,
indicator: 'orange',
title: __("Generate QRCode"),
message: __("Generate and attach QR Code using IRN?"),
primary_action: {
action: function() {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
args: { doctype, docname: name },
freeze: true,
callback: () => frm.reload_doc() || dialog.hide()
callback: () => frm.reload_doc() || dialog.hide(),
error: () => dialog.hide()
});
}
},
primary_action_label: __('Yes')
});
dialog.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
add_custom_button(__("Generate QRCode"), action);
}
}
});
@@ -167,85 +234,100 @@ erpnext.setup_einvoice_actions = (doctype) => {
const get_ewaybill_fields = (frm) => {
return [
{
'fieldname': 'transporter',
'label': 'Transporter',
'fieldtype': 'Link',
'options': 'Supplier',
'default': frm.doc.transporter
fieldname: "eway_part_a_section_break",
fieldtype: "Section Break",
label: "Part A",
},
{
'fieldname': 'gst_transporter_id',
'label': 'GST Transporter ID',
'fieldtype': 'Data',
'default': frm.doc.gst_transporter_id
fieldname: "transporter",
label: "Transporter",
fieldtype: "Link",
options: "Supplier",
default: frm.doc.transporter,
},
{
'fieldname': 'driver',
'label': 'Driver',
'fieldtype': 'Link',
'options': 'Driver',
'default': frm.doc.driver
fieldname: "transporter_name",
label: "Transporter Name",
fieldtype: "Data",
read_only: 1,
default: frm.doc.transporter_name,
depends_on: "transporter",
},
{
'fieldname': 'lr_no',
'label': 'Transport Receipt No',
'fieldtype': 'Data',
'default': frm.doc.lr_no
fieldname: "part_a_column_break",
fieldtype: "Column Break",
},
{
'fieldname': 'vehicle_no',
'label': 'Vehicle No',
'fieldtype': 'Data',
'default': frm.doc.vehicle_no
fieldname: "gst_transporter_id",
label: "GST Transporter ID",
fieldtype: "Data",
default: frm.doc.gst_transporter_id,
},
{
'fieldname': 'distance',
'label': 'Distance (in km)',
'fieldtype': 'Float',
'default': frm.doc.distance
fieldname: "distance",
label: "Distance (in km)",
fieldtype: "Float",
default: frm.doc.distance,
description: 'Set as zero to auto calculate distance using pin codes',
},
{
'fieldname': 'transporter_col_break',
'fieldtype': 'Column Break',
fieldname: "eway_part_b_section_break",
fieldtype: "Section Break",
label: "Part B",
},
{
'fieldname': 'transporter_name',
'label': 'Transporter Name',
'fieldtype': 'Data',
'read_only': 1,
'default': frm.doc.transporter_name,
'depends_on': 'transporter'
fieldname: "mode_of_transport",
label: "Mode of Transport",
fieldtype: "Select",
options: `\nRoad\nAir\nRail\nShip`,
default: frm.doc.mode_of_transport,
},
{
'fieldname': 'mode_of_transport',
'label': 'Mode of Transport',
'fieldtype': 'Select',
'options': `\nRoad\nAir\nRail\nShip`,
'default': frm.doc.mode_of_transport
fieldname: "gst_vehicle_type",
label: "GST Vehicle Type",
fieldtype: "Select",
options: `Regular\nOver Dimensional Cargo (ODC)`,
depends_on: 'eval:(doc.mode_of_transport === "Road")',
default: frm.doc.gst_vehicle_type,
},
{
'fieldname': 'driver_name',
'label': 'Driver Name',
'fieldtype': 'Data',
'fetch_from': 'driver.full_name',
'read_only': 1,
'default': frm.doc.driver_name,
'depends_on': 'driver'
fieldname: "vehicle_no",
label: "Vehicle No",
fieldtype: "Data",
default: frm.doc.vehicle_no,
},
{
'fieldname': 'lr_date',
'label': 'Transport Receipt Date',
'fieldtype': 'Date',
'default': frm.doc.lr_date
fieldname: "part_b_column_break",
fieldtype: "Column Break",
},
{
'fieldname': 'gst_vehicle_type',
'label': 'GST Vehicle Type',
'fieldtype': 'Select',
'options': `Regular\nOver Dimensional Cargo (ODC)`,
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.gst_vehicle_type
}
fieldname: "lr_date",
label: "Transport Receipt Date",
fieldtype: "Date",
default: frm.doc.lr_date,
},
{
fieldname: "lr_no",
label: "Transport Receipt No",
fieldtype: "Data",
default: frm.doc.lr_no,
},
{
fieldname: "driver",
label: "Driver",
fieldtype: "Link",
options: "Driver",
default: frm.doc.driver,
},
{
fieldname: "driver_name",
label: "Driver Name",
fieldtype: "Data",
fetch_from: "driver.full_name",
read_only: 1,
default: frm.doc.driver_name,
depends_on: "driver",
},
];
};

View File

@@ -168,7 +168,12 @@ def get_doc_details(invoice):
title=_("Not Allowed"),
)
invoice_type = "CRN" if invoice.is_return else "INV"
if invoice.is_return:
invoice_type = "CRN"
elif invoice.is_debit_note:
invoice_type = "DBN"
else:
invoice_type = "INV"
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
@@ -444,7 +449,7 @@ def get_eway_bill_details(invoice):
dict(
gstin=invoice.gst_transporter_id,
name=invoice.transporter_name,
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
mode_of_transport=mode_of_transport[invoice.mode_of_transport or ""] or None,
distance=invoice.distance or 0,
document_name=invoice.lr_no,
document_date=format_date(invoice.lr_date, "dd/mm/yyyy"),
@@ -791,8 +796,9 @@ class GSPConnector:
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
def set_invoice(self):
self.invoice = None
@@ -856,8 +862,8 @@ class GSPConnector:
return res
def auto_refresh_token(self):
self.fetch_auth_token()
self.token_auto_refreshed = True
self.fetch_auth_token()
def log_request(self, url, headers, data, res):
headers.update({"password": self.credentials.password})
@@ -997,6 +1003,37 @@ class GSPConnector:
return failed
def fetch_and_attach_qrcode_from_irn(self):
qrcode = self.get_qrcode_from_irn(self.invoice.irn)
if qrcode:
qrcode_file = self.create_qr_code_file(qrcode)
frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
else:
frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
def get_qrcode_from_irn(self, irn):
import requests
headers = self.get_headers()
headers.update({"width": "215", "height": "215", "imgtype": "jpg", "irn": irn})
try:
# using requests.get instead of make_request to avoid parsing the response
res = requests.get(self.get_qrcode_url, headers=headers)
self.log_request(self.get_qrcode_url, headers, None, None)
if res.status_code == 200:
return res.content
else:
raise RequestFailed(str(res.content, "utf-8"))
except RequestFailed as e:
self.raise_error(errors=str(e))
except Exception:
log_error()
self.raise_error()
def get_irn_details(self, irn):
headers = self.get_headers()
@@ -1112,6 +1149,19 @@ class GSPConnector:
self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
self.invoice.eway_bill_cancelled = 0
self.invoice.update(args)
if res.get("info"):
info = res.get("info")
# when we have more features (responses) in eway bill, we can add them using below forloop.
for msg in info:
if msg.get("InfCd") == "EWBPPD":
pin_to_pin_distance = int(re.search(r"\d+", msg.get("Desc")).group())
frappe.msgprint(
_("Auto Calculated Distance is {} KM.").format(str(pin_to_pin_distance)),
title="Notification",
indicator="green",
alert=True,
)
self.invoice.distance = flt(pin_to_pin_distance)
self.invoice.flags.updater_reference = {
"doctype": self.invoice.doctype,
"docname": self.invoice.name,
@@ -1134,7 +1184,6 @@ class GSPConnector:
headers = self.get_headers()
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
headers["username"] = headers["user_name"]
del headers["user_name"]
try:
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
if res.get("success"):
@@ -1225,13 +1274,18 @@ class GSPConnector:
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
docname = self.invoice.name
filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
qr_image = io.BytesIO()
url = qrcreate(qrcode, error="L")
url.png(qr_image, scale=2, quiet_zone=1)
qrcode_file = self.create_qr_code_file(qr_image.getvalue())
self.invoice.qrcode_image = qrcode_file.file_url
def create_qr_code_file(self, qr_image):
doctype = self.invoice.doctype
docname = self.invoice.name
filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
_file = frappe.get_doc(
{
"doctype": "File",
@@ -1240,12 +1294,12 @@ class GSPConnector:
"attached_to_name": docname,
"attached_to_field": "qrcode_image",
"is_private": 0,
"content": qr_image.getvalue(),
"content": qr_image,
}
)
_file.save()
frappe.db.commit()
self.invoice.qrcode_image = _file.file_url
return _file
def update_invoice(self):
self.invoice.flags.ignore_validate_update_after_submit = True
@@ -1290,6 +1344,12 @@ def cancel_irn(doctype, docname, irn, reason, remark):
gsp_connector.cancel_irn(irn, reason, remark)
@frappe.whitelist()
def generate_qrcode(doctype, docname):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.fetch_and_attach_qrcode_from_irn()
@frappe.whitelist()
def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector = GSPConnector(doctype, docname)
@@ -1297,13 +1357,9 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist()
def cancel_eway_bill(doctype, docname):
# TODO: uncomment when eway_bill api from Adequare is enabled
# gsp_connector = GSPConnector(doctype, docname)
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
frappe.db.set_value(doctype, docname, "ewaybill", "")
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
@frappe.whitelist()

View File

@@ -33,7 +33,7 @@ def _execute(filters=None):
added_item = []
for d in item_list:
if (d.parent, d.item_code) not in added_item:
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate]
total_tax = 0
for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
@@ -41,11 +41,9 @@ def _execute(filters=None):
row += [d.base_net_amount + total_tax]
row += [d.base_net_amount]
for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
row += [item_tax.get("tax_amount", 0)]
data.append(row)
added_item.append((d.parent, d.item_code))
if data:
@@ -65,6 +63,7 @@ def get_columns():
{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300},
{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100},
{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90},
{"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90},
{"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120},
{
"fieldname": "taxable_amount",
@@ -107,16 +106,25 @@ def get_items(filters):
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
`tabSales Invoice Item`.parent,
`tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description,
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail,
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate
from
`tabSales Invoice`,
`tabSales Invoice Item`,
`tabGST HSN Code`,
`tabSales Taxes and Charges`
where
`tabSales Invoice`.name = `tabSales Invoice Item`.parent
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
and `tabSales Invoice`.docstatus = 1
and `tabSales Invoice Item`.gst_hsn_code is not NULL
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
group by
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
`tabSales Invoice Item`.parent,
`tabSales Invoice Item`.item_code
"""
% (conditions, match_conditions),
filters,
@@ -214,15 +222,16 @@ def get_merged_data(columns, data):
result = []
for row in data:
merged_hsn_dict.setdefault(row[0], {})
key = row[0] + "-" + str(row[4])
merged_hsn_dict.setdefault(key, {})
for i, d in enumerate(columns):
if d["fieldtype"] not in ("Int", "Float", "Currency"):
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
merged_hsn_dict[key][d["fieldname"]] = row[i]
else:
if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""):
merged_hsn_dict[row[0]][d["fieldname"]] += row[i]
if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""):
merged_hsn_dict[key][d["fieldname"]] += row[i]
else:
merged_hsn_dict[row[0]][d["fieldname"]] = row[i]
merged_hsn_dict[key][d["fieldname"]] = row[i]
for key, value in iteritems(merged_hsn_dict):
result.append(value)
@@ -241,7 +250,7 @@ def get_json(filters, report_name, data):
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp}
gst_json = {"version": "GST3.0.3", "hash": "hash", "gstin": gstin, "fp": fp}
gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)}
@@ -272,7 +281,7 @@ def get_hsn_wise_json_data(filters, report_data):
"desc": hsn.get("description"),
"uqc": hsn.get("stock_uom").upper(),
"qty": hsn.get("stock_qty"),
"val": flt(hsn.get("total_amount"), 2),
"rt": flt(hsn.get("tax_rate"), 2),
"txval": flt(hsn.get("taxable_amount", 2)),
"iamt": 0.0,
"camt": 0.0,

View File

@@ -65,7 +65,11 @@ frappe.ui.form.on("Sales Order", {
frm.set_value('transaction_date', frappe.datetime.get_today())
}
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
return {
filters: [
["Warehouse", "company", "in", ["", cstr(frm.doc.company)]],
]
};
});
frm.set_query('project', function(doc, cdt, cdn) {
@@ -77,7 +81,19 @@ frappe.ui.form.on("Sales Order", {
}
});
erpnext.queries.setup_warehouse_query(frm);
frm.set_query('warehouse', 'items', function(doc, cdt, cdn) {
let row = locals[cdt][cdn];
let query = {
filters: [
["Warehouse", "company", "in", ["", cstr(frm.doc.company)]],
]
};
if (row.item_code) {
query.query = "erpnext.controllers.queries.warehouse_query";
query.filters.push(["Bin", "item_code", "=", row.item_code]);
}
return query;
});
frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
},

View File

@@ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class {
frappe.dom.freeze();
this.frm = this.get_new_frm(this.frm);
this.frm.doc.items = [];
const res = await frappe.call({
return frappe.call({
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
args: {
'source_name': doc.name,
'target_doc': this.frm.doc
},
callback: (r) => {
frappe.model.sync(r.message);
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
this.set_pos_profile_data().then(() => {
frappe.dom.unfreeze();
});
}
});
frappe.model.sync(res.message);
await this.set_pos_profile_data();
frappe.dom.unfreeze();
}
set_pos_profile_data() {

View File

@@ -4,10 +4,13 @@
frappe.ui.form.on("Naming Series", {
onload: function(frm) {
frm.disable_save();
frm.events.get_doc_and_prefix(frm);
},
refresh: function(frm) {
frm.disable_save();
},
get_doc_and_prefix: function(frm) {
frappe.call({
method: "get_transactions",

View File

@@ -16,6 +16,9 @@ from erpnext.manufacturing.doctype.production_plan.test_production_plan import m
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
@@ -180,9 +183,12 @@ def make_items():
if not frappe.db.exists("Item", item_code):
create_item(item_code)
create_stock_reconciliation(
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
)
try:
create_stock_reconciliation(
item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
)
except EmptyStockReconciliationItemsError:
pass
if frappe.db.exists("Item", "Test FG A RW 1"):
doc = frappe.get_doc("Item", "Test FG A RW 1")

View File

@@ -653,6 +653,104 @@ class TestStockEntry(FrappeTestCase):
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
def test_serial_batch_item_stock_entry(self):
"""
Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
2) Cancel same Stock Entry
Expected Result: 1) Batch is created with Reference in Serial No
2) Batch is deleted and Serial No is Inactive
"""
from erpnext.stock.doctype.batch.batch import get_batch_qty
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
if not item:
item = create_item("Batched and Serialised Item")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "B-BATCH-.##"
item.serial_no_series = "S-.####"
item.save()
else:
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
se = make_stock_entry(
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
)
batch_no = se.items[0].batch_no
serial_no = get_serial_nos(se.items[0].serial_no)[0]
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
self.assertEqual(batch_in_serial_no, batch_no)
self.assertEqual(batch_qty, 1)
se.cancel()
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
self.assertEqual(batch_in_serial_no, None)
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
def test_serial_batch_item_qty_deduction(self):
"""
Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch
Expected: 1) Cancelling first Stock Entry (origin transaction of created batch)
should throw a LinkExistsError
2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch
and in that transaction only, Inactive.
"""
from erpnext.stock.doctype.batch.batch import get_batch_qty
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
if not item:
item = create_item("Batched and Serialised Item")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "B-BATCH-.##"
item.serial_no_series = "S-.####"
item.save()
else:
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
se1 = make_stock_entry(
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
)
batch_no = se1.items[0].batch_no
serial_no1 = get_serial_nos(se1.items[0].serial_no)[0]
# Check Source (Origin) Document of Batch
self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name)
se2 = make_stock_entry(
item_code=item.item_code,
target="_Test Warehouse - _TC",
qty=1,
basic_rate=100,
batch_no=batch_no,
)
serial_no2 = get_serial_nos(se2.items[0].serial_no)[0]
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
self.assertEqual(batch_qty, 2)
se2.cancel()
# Check decrease in Batch Qty
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
self.assertEqual(batch_qty, 1)
# Check if Serial No from Stock Entry 1 is intact
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no)
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active")
# Check if Serial No from Stock Entry 2 is Unlinked and Inactive
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None)
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
def test_warehouse_company_validation(self):
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
frappe.get_doc("User", "test2@example.com").add_roles(

View File

@@ -61,6 +61,7 @@ class StockReconciliation(StockController):
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
@@ -455,7 +456,7 @@ class StockReconciliation(StockController):
key = (d.item_code, d.warehouse)
if key not in merge_similar_entries:
d.total_amount = d.actual_qty * d.valuation_rate
d.total_amount = flt(d.actual_qty) * d.valuation_rate
merge_similar_entries[key] = d
elif d.serial_no:
data = merge_similar_entries[key]

View File

@@ -251,7 +251,7 @@ class TestStockReconciliation(FrappeTestCase):
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1
item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_save=1
)
sr.save(ignore_permissions=True)
sr.submit()
@@ -287,6 +287,84 @@ class TestStockReconciliation(FrappeTestCase):
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
def test_stock_reco_for_serial_and_batch_item(self):
item = create_item("_TestBatchSerialItemReco")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "TBS-BATCH-.##"
item.serial_no_series = "TBS-.####"
item.save()
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(item_code=item.item_code, warehouse=warehouse, qty=1, rate=100)
batch_no = sr.items[0].batch_no
serial_nos = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos), 1)
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no)
sr.cancel()
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
"""
Behaviour: 1) Create Stock Reconciliation, which will be the origin document
of a new batch having a serial no
2) Create a Stock Entry that adds a serial no to the same batch following this
Stock Reconciliation
3) Cancel Stock Entry
Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases
"""
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item = create_item("_TestBatchSerialItemDependentReco")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "TBSD-BATCH-.##"
item.serial_no_series = "TBSD-.####"
item.save()
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
stock_reco = create_stock_reconciliation(
item_code=item.item_code, warehouse=warehouse, qty=1, rate=100
)
batch_no = stock_reco.items[0].batch_no
reco_serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0]
stock_entry = make_stock_entry(
item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no
)
serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0]
# Check Batch qty after 2 transactions
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
self.assertEqual(batch_qty, 2)
# Cancel latest stock document
stock_entry.cancel()
# Check Batch qty after cancellation
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
self.assertEqual(batch_qty, 1)
# Check if Serial No from Stock Reconcilation is intact
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "batch_no"), batch_no)
self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "status"), "Active")
# Check if Serial No from Stock Entry is Unlinked and Inactive
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None)
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive")
stock_reco.cancel()
def test_customer_provided_items(self):
item_code = "Stock-Reco-customer-Item-100"
create_item(
@@ -683,11 +761,13 @@ def create_stock_reconciliation(**args):
},
)
try:
if not args.do_not_submit:
sr.submit()
except EmptyStockReconciliationItemsError:
pass
if not args.do_not_save:
sr.insert()
try:
if not args.do_not_submit:
sr.submit()
except EmptyStockReconciliationItemsError:
pass
return sr