Merge branch 'develop' into party

This commit is contained in:
Chillar Anand 2022-03-08 09:53:03 +05:30 committed by GitHub
commit 282e9f83b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 686 additions and 141 deletions

View File

@ -346,6 +346,8 @@ frappe.ui.form.on('Payment Entry', {
}
frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
@ -379,7 +381,11 @@ frappe.ui.form.on('Payment Entry', {
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
}
},
() => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
frm.doc.paid_from_account_currency, company_currency),
() => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency)
]);
}
}
@ -483,14 +489,14 @@ frappe.ui.form.on('Payment Entry', {
},
paid_from_account_currency: function(frm) {
if(!frm.doc.paid_from_account_currency) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
@ -510,8 +516,8 @@ frappe.ui.form.on('Payment Entry', {
},
paid_to_account_currency: function(frm) {
if(!frm.doc.paid_to_account_currency) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency);

View File

@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty'
})
self.status_updater.append({
'source_dt': 'Purchase Order Item',
'target_dt': 'Packed Item',
'target_field': 'ordered_qty',
'target_parent_dt': 'Sales Order',
'target_parent_field': '',
'join_field': 'sales_order_packed_item',
'target_ref_field': 'qty',
'source_field': 'stock_qty'
})
def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship"""

View File

@ -63,6 +63,7 @@
"material_request_item",
"sales_order",
"sales_order_item",
"sales_order_packed_item",
"supplier_quotation",
"supplier_quotation_item",
"col_break5",
@ -837,21 +838,30 @@
"label": "Product Bundle",
"options": "Product Bundle",
"read_only": 1
},
{
"fieldname": "sales_order_packed_item",
"fieldtype": "Data",
"label": "Sales Order Packed Item",
"no_copy": 1,
"print_hide": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-30 20:06:26.712097",
"modified": "2022-02-02 13:10:18.398976",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -6,6 +6,7 @@ import copy
import frappe
from frappe import _
from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import date_diff, flt, getdate
@ -16,12 +17,9 @@ def execute(filters=None):
validate_filters(filters)
columns = get_columns(filters)
conditions = get_conditions(filters)
data = get_data(filters)
#get queried data
data = get_data(filters, conditions)
#prepare data for report and chart views
# prepare data for report and chart views
data, chart_data = prepare_data(data, filters)
return columns, data, None, chart_data
@ -34,53 +32,70 @@ def validate_filters(filters):
elif date_diff(to_date, from_date) < 0:
frappe.throw(_("To Date cannot be before From Date."))
def get_conditions(filters):
conditions = ''
def get_data(filters):
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
query = (
frappe.qb.from_(mr)
.join(mr_item).on(mr_item.parent == mr.name)
.select(
mr.name.as_("material_request"),
mr.transaction_date.as_("date"),
mr_item.schedule_date.as_("required_date"),
mr_item.item_code.as_("item_code"),
Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
Coalesce(mr_item.stock_uom, '').as_("uom"),
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))
).as_("qty_to_receive"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))
).as_("qty_to_order"),
mr_item.item_name,
mr_item.description,
mr.company
).where(
(mr.material_request_type == "Purchase")
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.per_received < 100)
)
)
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
query = (
query.groupby(
mr.name, mr_item.item_code
).orderby(
mr.transaction_date, mr.schedule_date
)
)
data = query.run(as_dict=True)
return data
def get_conditions(filters, query, mr, mr_item):
if filters.get("from_date") and filters.get("to_date"):
conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date"))
query = (
query.where(
(mr.transaction_date >= filters.get("from_date"))
& (mr.transaction_date <= filters.get("to_date"))
)
)
if filters.get("company"):
conditions += " and mr.company = '{0}'".format(filters.get("company"))
query = query.where(mr.company == filters.get("company"))
if filters.get("material_request"):
conditions += " and mr.name = '{0}'".format(filters.get("material_request"))
query = query.where(mr.name == filters.get("material_request"))
if filters.get("item_code"):
conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code"))
query = query.where(mr_item.item_code == filters.get("item_code"))
return conditions
def get_data(filters, conditions):
data = frappe.db.sql("""
select
mr.name as material_request,
mr.transaction_date as date,
mr_item.schedule_date as required_date,
mr_item.item_code as item_code,
sum(ifnull(mr_item.stock_qty, 0)) as qty,
ifnull(mr_item.stock_uom, '') as uom,
sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty,
sum(ifnull(mr_item.received_qty, 0)) as received_qty,
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive,
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order,
mr_item.item_name as item_name,
mr_item.description as "description",
mr.company as company
from
`tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where
mr_item.parent = mr.name
and mr.material_request_type = "Purchase"
and mr.docstatus = 1
and mr.status != "Stopped"
{conditions}
group by mr.name, mr_item.item_code
having
sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0))
order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1)
return data
return query
def update_qty_columns(row_to_update, data_row):
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]

View File

@ -0,0 +1,69 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
get_data,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
def setUp(self) -> None:
create_item("Test MR Report Item")
self.setup_material_request() # to order and receive
self.setup_material_request(order=True) # to receive (ordered)
self.setup_material_request(order=True, receive=True) # complete (ordered & received)
self.filters = frappe._dict(
company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
item_code="Test MR Report Item"
)
def tearDown(self) -> None:
frappe.db.rollback()
def test_date_range(self):
data = get_data(self.filters)
self.assertEqual(len(data), 2) # MRs today should be fetched
self.filters.from_date = add_days(today(), 1)
data = get_data(self.filters)
self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow
def test_ordered_received_material_requests(self):
data = get_data(self.filters)
# from the 3 MRs made, only 2 (to receive) should be fetched
self.assertEqual(len(data), 2)
self.assertEqual(data[0].ordered_qty, 0.0)
self.assertEqual(data[1].ordered_qty, 57.0)
def setup_material_request(self, order=False, receive=False):
po = None
test_records = frappe.get_test_records('Material Request')
mr = frappe.copy_doc(test_records[0])
mr.transaction_date = today()
mr.schedule_date = add_days(today(), 1)
for row in mr.items:
row.item_code = "Test MR Report Item"
row.item_name = "Test MR Report Item"
row.description = "Test MR Report Item"
row.uom = "Nos"
row.schedule_date = add_days(today(), 1)
mr.submit()
if order or receive:
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.submit()
if receive:
pr = make_purchase_receipt(po.name)
pr.submit()

View File

@ -507,13 +507,41 @@ class StockController(AccountsController):
"voucher_no": self.name,
"company": self.company
})
if future_sle_exists(args):
if future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
if item_based_reposting:
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
else:
create_repost_item_valuation_entry(args)
def repost_required_for_queue(doc: StockController) -> bool:
"""check if stock document contains repeated item-warehouse with queue based valuation.
if queue exists for repeated items then SLEs need to reprocessed in background again.
"""
consuming_sles = frappe.db.get_all("Stock Ledger Entry",
filters={
"voucher_type": doc.doctype,
"voucher_no": doc.name,
"actual_qty": ("<", 0),
"is_cancelled": 0
},
fields=["item_code", "warehouse", "stock_queue"]
)
item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
unique_item_warehouses = set(item_warehouses)
if len(unique_item_warehouses) == len(item_warehouses):
return False
for sle in consuming_sles:
if sle.stock_queue != "[]": # using FIFO/LIFO valuation
return True
return False
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):

View File

@ -113,17 +113,24 @@ class calculate_taxes_and_totals(object):
for item in self.doc.get("items"):
self.doc.round_floats_in(item)
if not item.rate:
item.rate = item.price_list_rate
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
if item.pricing_rules or abs(item.discount_percentage) > 0:
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount and item.pricing_rules:
if abs(item.discount_percentage) > 0:
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount or item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))

View File

@ -174,16 +174,22 @@ def get_month_map():
def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar
month_map = get_month_map()
today = get_datetime()
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)]
joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"])
start_day = 1
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
length = len(dates_of_month)
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
if joining_date and joining_date.month == month_map[month]:
start_day = joining_date.day
if relieving_date and relieving_date.month == month_map[month]:
end_day = relieving_date.day + 1
records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)]
month_start, month_end = dates_of_month[0], dates_of_month[-1]
records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[
["attendance_date", ">=", month_start],
["attendance_date", "<=", month_end],
["employee", "=", employee],
@ -200,7 +206,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
for date in dates_of_month:
date_time = get_datetime(date)
if today.day == date_time.day and today.month == date_time.month:
if today.day <= date_time.day and today.month <= date_time.month:
break
if date_time not in marked_days:
unmarked_days.append(date)

View File

@ -4,17 +4,104 @@
import unittest
import frappe
from frappe.utils import nowdate
from frappe.utils import add_days, get_first_day, getdate, nowdate
from erpnext.hr.doctype.attendance.attendance import (
get_month_map,
get_unmarked_days,
mark_attendance,
)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
test_records = frappe.get_test_records('Attendance')
class TestAttendance(unittest.TestCase):
def test_mark_absent(self):
from erpnext.hr.doctype.employee.test_employee import make_employee
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
from erpnext.hr.doctype.attendance.attendance import mark_attendance
attendance = mark_attendance(employee, date, 'Absent')
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
self.assertEqual(attendance, fetch_attendance)
def test_unmarked_days(self):
first_day = get_first_day(getdate())
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
first_sunday = get_first_sunday(holiday_list)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
unmarked_days = get_unmarked_days(employee, month_name)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
self.assertNotIn(first_day, unmarked_days)
# attendance unmarked
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
# holiday considered in unmarked days
self.assertIn(first_sunday, unmarked_days)
def test_unmarked_days_excluding_holidays(self):
first_day = get_first_day(getdate())
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
first_sunday = get_first_sunday(holiday_list)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
self.assertNotIn(first_day, unmarked_days)
# attendance unmarked
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
# holidays not considered in unmarked days
self.assertNotIn(first_sunday, unmarked_days)
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
first_day = get_first_day(getdate())
doj = add_days(first_day, 1)
relieving_date = add_days(first_day, 5)
employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj,
date_of_relieving=relieving_date)
frappe.db.delete('Attendance', {'employee': employee})
attendance_date = add_days(first_day, 2)
mark_attendance(employee, attendance_date, 'Present')
month_name = get_month_name(first_day)
unmarked_days = get_unmarked_days(employee, month_name)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
self.assertNotIn(attendance_date, unmarked_days)
# date before doj not in unmarked days
self.assertNotIn(add_days(doj, -1), unmarked_days)
# date after relieving not in unmarked days
self.assertNotIn(add_days(relieving_date, 1), unmarked_days)
def tearDown(self):
frappe.db.rollback()
def get_month_name(date):
month_number = date.month
for month, number in get_month_map().items():
if number == month_number:
return month

View File

@ -7,7 +7,7 @@ def get_data():
'transactions': [
{
'label': _('Manufacture'),
'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet']
'items': ['BOM', 'Work Order', 'Job Card']
}
]
}

View File

@ -17,7 +17,7 @@ frappe.ui.form.on('Routing', {
},
calculate_operating_cost: function(frm, child) {
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child));
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
}
});

View File

@ -20,7 +20,8 @@ class Routing(Document):
for operation in self.operations:
if not operation.hour_rate:
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
operation.precision("operating_cost"))
def set_routing_id(self):
sequence_id = 0

View File

@ -11,9 +11,9 @@ def get_data():
},
{
'label': _('Transaction'),
'items': ['Work Order', 'Job Card', 'Timesheet']
'items': ['Work Order', 'Job Card',]
}
],
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
'Work Order', 'Job Card', 'Timesheet']
'Work Order', 'Job Card',]
}

View File

@ -307,28 +307,59 @@ class SalarySlip(TransactionBase):
if payroll_based_on == "Attendance":
self.payment_days -= flt(absent)
unmarked_days = self.get_unmarked_days()
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days)
self.absent_days += unmarked_days #will be treated as absent
self.payment_days -= unmarked_days
if include_holidays_in_total_working_days:
for holiday in holidays:
if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
self.payment_days += 1
else:
self.payment_days = 0
def get_unmarked_days(self):
marked_days = frappe.get_all("Attendance", filters = {
"attendance_date": ["between", [self.start_date, self.end_date]],
"employee": self.employee,
"docstatus": 1
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
def get_unmarked_days(self, include_holidays_in_total_working_days):
unmarked_days = self.total_working_days
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
start_date = self.start_date
end_date = self.end_date
return self.total_working_days - marked_days
if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)):
start_date = joining_date
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
include_holidays_in_total_working_days, self.start_date, joining_date)
if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)):
end_date = relieving_date
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
include_holidays_in_total_working_days, relieving_date, self.end_date)
# exclude days for which attendance has been marked
unmarked_days -= frappe.get_all("Attendance", filters = {
"attendance_date": ["between", [start_date, end_date]],
"employee": self.employee,
"docstatus": 1
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
return unmarked_days
def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days,
include_holidays_in_total_working_days, start_date, end_date):
"""
Exclude days before DOJ or after
Relieving Date from unmarked days
"""
from erpnext.hr.doctype.employee.employee import is_holiday
if include_holidays_in_total_working_days:
unmarked_days -= date_diff(end_date, start_date)
else:
# exclude only if not holidays
for days in range(date_diff(end_date, start_date)):
date = add_days(end_date, -days)
if not is_holiday(self.employee, date):
unmarked_days -= 1
return unmarked_days
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
if not joining_date:

View File

@ -7,10 +7,12 @@ import unittest
import frappe
from frappe.model.document import Document
from frappe.tests.utils import change_settings
from frappe.utils import (
add_days,
add_months,
cstr,
date_diff,
flt,
get_first_day,
get_last_day,
@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random
import erpnext
from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@ -37,17 +40,17 @@ class TestSalarySlip(unittest.TestCase):
setup_test()
def tearDown(self):
frappe.db.rollback()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator")
@change_settings("Payroll Settings", {
"payroll_based_on": "Attendance",
"daily_wages_fraction_for_half_day": 0.75
})
def test_payment_days_based_on_attendance(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
no_of_days = self.get_no_of_days()
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75)
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@ -85,14 +88,78 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.gross_pay, gross_pay)
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
@change_settings("Payroll Settings", {
"payroll_based_on": "Attendance",
"consider_unmarked_attendance_as": "Absent",
"include_holidays_in_total_working_days": True
})
def test_payment_days_for_mid_joinee_including_holidays(self):
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
no_of_days = self.get_no_of_days()
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
frappe.db.set_value("Employee", new_emp_id, {
"date_of_joining": joining_date,
"relieving_date": relieving_date,
"status": "Left"
})
holidays = 0
for days in range(date_diff(relieving_date, joining_date) + 1):
date = add_days(joining_date, days)
if not is_holiday("Salary Slip Test Holiday List", date):
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
else:
holidays += 1
new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
self.assertEqual(new_ss.total_working_days, no_of_days[0])
self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
@change_settings("Payroll Settings", {
"payroll_based_on": "Attendance",
"consider_unmarked_attendance_as": "Absent",
"include_holidays_in_total_working_days": False
})
def test_payment_days_for_mid_joinee_excluding_holidays(self):
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
no_of_days = self.get_no_of_days()
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
frappe.db.set_value("Employee", new_emp_id, {
"date_of_joining": joining_date,
"relieving_date": relieving_date,
"status": "Left"
})
holidays = 0
for days in range(date_diff(relieving_date, joining_date) + 1):
date = add_days(joining_date, days)
if not is_holiday("Salary Slip Test Holiday List", date):
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
else:
holidays += 1
new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1])
self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
@change_settings("Payroll Settings", {
"payroll_based_on": "Leave"
})
def test_payment_days_based_on_leave_application(self):
no_of_days = self.get_no_of_days()
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@ -133,8 +200,9 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
@change_settings("Payroll Settings", {
"payroll_based_on": "Attendance"
})
def test_payment_days_in_salary_slip_based_on_timesheet(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.projects.doctype.timesheet.test_timesheet import (
@ -145,9 +213,6 @@ class TestSalarySlip(unittest.TestCase):
make_salary_slip as make_salary_slip_for_timesheet,
)
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
@ -185,17 +250,15 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
@change_settings("Payroll Settings", {
"payroll_based_on": "Attendance"
})
def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
@ -238,11 +301,12 @@ class TestSalarySlip(unittest.TestCase):
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
self.assertEqual(actual_amount, expected_amount)
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
@change_settings("Payroll Settings", {
"include_holidays_in_total_working_days": 1
})
def test_salary_slip_with_holidays_included(self):
no_of_days = self.get_no_of_days()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
make_employee("test_salary_slip_with_holidays_included@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None)
@ -256,9 +320,11 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000)
@change_settings("Payroll Settings", {
"include_holidays_in_total_working_days": 0
})
def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None)
@ -273,14 +339,15 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000)
@change_settings("Payroll Settings", {
"include_holidays_in_total_working_days": 1
})
def test_payment_days(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
no_of_days = self.get_no_of_days()
# Holidays not included in working days
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
# set joinng date in the same month
employee = make_employee("test_payment_days@salary.com")
@ -338,11 +405,12 @@ class TestSalarySlip(unittest.TestCase):
frappe.set_user("test_employee_salary_slip_read_permission@salary.com")
self.assertTrue(salary_slip_test_employee.has_permission("read"))
@change_settings("Payroll Settings", {
"email_salary_slip_to_employee": 1
})
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1)
make_employee("test_email_salary_slip@salary.com")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company"

View File

@ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", {
currency: function(frm) {
let base_currency = frappe.defaults.get_global_default('currency');
if (base_currency != frm.doc.currency) {
if (frm.doc.currency && (base_currency != frm.doc.currency)) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {

View File

@ -23,9 +23,5 @@
"StateCesAmt": "{item.state_cess_amount}",
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
"OthChrg": "{item.other_charges}",
"TotItemVal": "{item.total_value}",
"BchDtls": {{
"Nm": "{item.batch_no}",
"ExpDt": "{item.batch_expiry_date}"
}}
"TotItemVal": "{item.total_value}"
}}

View File

@ -214,8 +214,6 @@ def get_item_list(invoice):
item.taxable_value = abs(item.taxable_value)
item.discount_amount = 0
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
item.serial_no = ""

View File

@ -40,7 +40,6 @@ frappe.ui.form.on('Quotation', {
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
onload(doc, dt, dn) {
var me = this;
super.onload(doc, dt, dn);
}
party_name() {

View File

@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
var me = this;
var dialog = new frappe.ui.Dialog({
title: __("Select Items"),
size: "large",
fields: [
{
"fieldtype": "Check",
@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} else {
let po_items = [];
me.frm.doc.items.forEach(d => {
let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) {
po_items.push({
"doctype": "Sales Order Item",
@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
dialog.show();
}
get_ordered_qty(item, so) {
let ordered_qty = item.ordered_qty;
if (so.packed_items) {
// calculate ordered qty based on packed items in case of product bundle
let packed_items = so.packed_items.filter(
(pi) => pi.parent_detail_docname == item.name
);
if (packed_items) {
ordered_qty = packed_items.reduce(
(sum, pi) => sum + flt(pi.ordered_qty),
0
);
ordered_qty = ordered_qty / packed_items.length;
}
}
return ordered_qty;
}
hold_sales_order(){
var me = this;
var d = new frappe.ui.Dialog({

View File

@ -877,6 +877,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project
def update_item_for_packed_item(source, target, source_parent):
target.qty = flt(source.qty) - flt(source.ordered_qty)
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
@ -920,6 +923,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"Packed Item": {
"doctype": "Purchase Order Item",
"field_map": [
["name", "sales_order_packed_item"],
["parent", "sales_order"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
@ -934,6 +938,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"supplier",
"pricing_rules"
],
"postprocess": update_item_for_packed_item,
"condition": lambda doc: doc.parent_item in items_to_map
}
}, target_doc, set_missing_values)

View File

@ -959,6 +959,42 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
def test_purchase_order_updates_packed_item_ordered_qty(self):
"""
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
"""
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle",
["_Test Bundle Item 1", "_Test Bundle Item 2"])
so_items = [
{
"item_code": product_bundle.item_code,
"warehouse": "",
"qty": 2,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
}
]
so = make_sales_order(item_list=so_items)
purchase_order = make_purchase_order(so.name, selected_items=so_items)
purchase_order.supplier = "_Test Supplier"
purchase_order.set_warehouse = "_Test Warehouse - _TC"
purchase_order.save()
purchase_order.submit()
so.reload()
self.assertEqual(so.packed_items[0].ordered_qty, 2)
self.assertEqual(so.packed_items[1].ordered_qty, 2)
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])

View File

@ -3,7 +3,6 @@
import json
import os
import frappe
import frappe.defaults
@ -422,14 +421,14 @@ def get_name_with_abbr(name, company):
return " - ".join(parts)
def install_country_fixtures(company, country):
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
if os.path.exists(path.encode("utf-8")):
try:
module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
frappe.get_attr(module_name)(company, False)
except Exception as e:
frappe.log_error()
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
try:
module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup"
frappe.get_attr(module_name)(company, False)
except ImportError:
pass
except Exception:
frappe.log_error()
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
def update_company_current_month_sales(company):

View File

@ -11,6 +11,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.utils import update_gl_entries_after
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
@ -177,6 +178,53 @@ class TestLandedCostVoucher(FrappeTestCase):
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
def test_serialized_lcv_delivered(self):
"""In some cases you'd want to deliver before you can know all the
landed costs, this should be allowed for serial nos too.
Case:
- receipt a serial no @ X rate
- delivery the serial no @ X rate
- add LCV to receipt X + Y
- LCV should be successful
- delivery should reflect X+Y valuation.
"""
serial_no = "LCV_TEST_SR_NO"
item_code = "_Test Serialized Item"
warehouse = "Stores - TCP1"
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse=warehouse, qty=1, rate=200,
item_code=item_code, serial_no=serial_no)
serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
# deliver it before creating LCV
dn = create_delivery_note(item_code=item_code,
company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
serial_no=serial_no, qty=1, rate=500,
cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
charges = 10
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + charges
serial_no = frappe.db.get_value("Serial No", serial_no,
["warehouse", "purchase_rate"], as_dict=1)
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
filters={
"voucher_no": dn.name,
"voucher_type": dn.doctype,
"is_cancelled": 0 # LCV cancels with same name.
},
fieldname="stock_value_difference")
# reposting should update the purchase rate in future delivery
self.assertEqual(stock_value_difference, -new_purchase_rate)
def test_landed_cost_voucher_for_odd_numbers (self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)

View File

@ -626,13 +626,13 @@ class TestMaterialRequest(FrappeTestCase):
mr.schedule_date = today()
if not frappe.db.get_value('UOM Conversion Detail',
{'parent': item.item_code, 'uom': 'Kg'}):
item_doc = frappe.get_doc('Item', item.item_code)
item_doc.append('uoms', {
'uom': 'Kg',
'conversion_factor': 5
})
item_doc.save(ignore_permissions=True)
{'parent': item.item_code, 'uom': 'Kg'}):
item_doc = frappe.get_doc('Item', item.item_code)
item_doc.append('uoms', {
'uom': 'Kg',
'conversion_factor': 5
})
item_doc.save(ignore_permissions=True)
item.uom = 'Kg'
for item in mr.items:

View File

@ -26,6 +26,7 @@
"section_break_13",
"actual_qty",
"projected_qty",
"ordered_qty",
"column_break_16",
"incoming_rate",
"page_break",
@ -224,13 +225,21 @@
"label": "Rate",
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "ordered_qty",
"fieldtype": "Float",
"label": "Ordered Qty",
"no_copy": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-28 16:03:30.780111",
"modified": "2022-02-22 12:57:45.325488",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",

View File

@ -161,6 +161,15 @@ class TestPurchaseReceipt(FrappeTestCase):
qty=abs(existing_bin_qty)
)
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
"Bin",
{
"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"
},
["actual_qty", "stock_value"]
)
pr = make_purchase_receipt()
stock_value_difference = frappe.db.get_value(

View File

@ -389,10 +389,13 @@ class TestStockLedgerEntry(FrappeTestCase):
)
def assertSLEs(self, doc, expected_sles):
def assertSLEs(self, doc, expected_sles, sle_filters=None):
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
if sle_filters:
filters.update(sle_filters)
sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
order_by="timestamp(posting_date, posting_time), creation")
for exp_sle, act_sle in zip(expected_sles, sles):
@ -665,6 +668,78 @@ class TestStockLedgerEntry(FrappeTestCase):
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
]))
def test_fifo_dependent_consumption(self):
item = make_item("_TestFifoTransferRates")
source = "_Test Warehouse - _TC"
target = "Stores - _TC"
rates = [10 * i for i in range(1, 20)]
receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
for rate in rates[1:]:
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
row.basic_rate = rate
receipt.append("items", row)
receipt.save()
receipt.submit()
expected_queues = []
for idx, rate in enumerate(rates, start=1):
expected_queues.append(
{"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
)
self.assertSLEs(receipt, expected_queues)
transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
for rate in rates[1:]:
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
transfer.append("items", row)
transfer.save()
transfer.submit()
# same exact queue should be transferred
self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
def test_fifo_multi_item_repack_consumption(self):
rm = make_item("_TestFifoRepackRM")
packed = make_item("_TestFifoRepackFinished")
warehouse = "_Test Warehouse - _TC"
rates = [10 * i for i in range(1, 5)]
receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
for rate in rates[1:]:
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
row.basic_rate = rate
receipt.append("items", row)
receipt.save()
receipt.submit()
repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
do_not_save=True, rate=10, purpose="Repack")
for rate in rates[1:]:
row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
repack.append("items", row)
repack.append("items", {
"item_code": packed.name,
"t_warehouse": warehouse,
"qty": 1,
"transfer_qty": 1,
})
repack.save()
repack.submit()
# same exact queue should be transferred
self.assertSLEs(repack, [
{"incoming_rate": sum(rates) * 10}
], sle_filters={"item_code": packed.name})
def create_repack_entry(**args):
args = frappe._dict(args)
repack = frappe.new_doc("Stock Entry")

View File

@ -1,4 +1,4 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe

View File

@ -21,6 +21,7 @@ SLE_FIELDS = (
"stock_value",
"stock_value_difference",
"valuation_rate",
"voucher_detail_no",
)
@ -66,7 +67,9 @@ def add_invariant_check_fields(sles):
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
balance_qty = sle.qty_after_transaction
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
if balance_qty is None:
balance_qty = sle.qty_after_transaction
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value

View File

@ -28,6 +28,16 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError):
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
""" Create SL entries from SL entry dicts
args:
- allow_negative_stock: disable negative stock valiations if true
- via_landed_cost_voucher: landed cost voucher cancels and reposts
entries of purchase document. This flag is used to identify if
cancellation and repost is happening via landed cost voucher, in
such cases certain validations need to be ignored (like negative
stock)
"""
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
cancel = sl_entries[0].get("is_cancelled")
@ -39,7 +49,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
future_sle_exists(args, sl_entries)
for sle in sl_entries:
if sle.serial_no:
if sle.serial_no and not via_landed_cost_voucher:
validate_serial_no(sle)
if cancel: