Merge branch 'develop' into crm-carry-forward-communication-comments
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index e7371fb..4211bd0 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -218,6 +218,8 @@
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
+
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
@@ -226,7 +228,7 @@
total_amount += get_paid_amount(frappe._dict({
'payment_document': voucher['payment_doctype'],
'payment_entry': voucher['payment_name'],
- }), transaction.currency)
+ }), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount:
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
@@ -261,7 +263,7 @@
return matching
def check_matching(bank_account, company, transaction, document_types):
- # combine all types of vocuhers
+ # combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types)
filters = {
"amount": transaction.unallocated_amount,
@@ -343,13 +345,11 @@
def get_je_matching_query(amount_condition, transaction):
# get matching journal entry query
+ # We have mapping at the bank level
+ # So one bank could have both types of bank accounts like asset and liability
+ # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
- root_type = frappe.get_value("Account", company_account, "root_type")
-
- if root_type == "Liability":
- cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
- else:
- cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+ cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 4620087..44cea31 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -102,7 +102,7 @@
AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
-def get_paid_amount(payment_entry, currency):
+def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@@ -115,7 +115,7 @@
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry":
- return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")
+ return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index aa2408e..cb18dd3 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -986,7 +986,7 @@
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
pi.set_posting_time = 1
- pi.posting_date = '2019-03-15'
+ pi.posting_date = '2019-01-10'
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2019-01-10"
pi.items[0].service_end_date = "2019-03-15"
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index beb38e2..f9b295a 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -409,7 +409,7 @@
def set_expired_status():
frappe.db.sql("""
UPDATE
- `tabMembership` SET `status` = 'Expired'
+ `tabMembership` SET `membership_status` = 'Expired'
WHERE
- `status` not in ('Cancelled') AND `to_date` < %s
+ `membership_status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate()))
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 3052a2b..6e8fae0 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -171,6 +171,7 @@
salary_slip.end_date = month_end_date
salary_slip.save()
salary_slip.submit()
+ salary_slip.reload()
no_of_days = self.get_no_of_days()
days_in_month = no_of_days[0]
diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py
index 0415690..1eb3d0d 100644
--- a/erpnext/projects/report/project_profitability/test_project_profitability.py
+++ b/erpnext/projects/report/project_profitability/test_project_profitability.py
@@ -25,6 +25,7 @@
self.timesheet = make_timesheet(emp, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
+ self.salary_slip.start_date = self.timesheet.start_date
holidays = self.salary_slip.get_holidays_for_employee(date, date)
if holidays:
@@ -41,8 +42,8 @@
def test_project_profitability(self):
filters = {
'company': '_Test Company',
- 'start_date': add_days(getdate(), -3),
- 'end_date': getdate()
+ 'start_date': add_days(self.timesheet.start_date, -3),
+ 'end_date': self.timesheet.start_date
}
report = execute(filters)
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index a3d44af..6e1e0d4 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -1,7 +1,6 @@
{
"actions": [],
"allow_import": 1,
- "allow_rename": 1,
"autoname": "field:serial_no",
"creation": "2013-05-16 10:59:15",
"description": "Distinct unit of an Item",
@@ -434,10 +433,11 @@
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
- "modified": "2021-01-08 14:31:15.375996",
+ "modified": "2021-12-23 10:44:30.299450",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -476,5 +476,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 38291d1..2947faf 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -194,23 +194,6 @@
if sle_exists:
frappe.throw(_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name))
- def before_rename(self, old, new, merge=False):
- if merge:
- frappe.throw(_("Sorry, Serial Nos cannot be merged"))
-
- def after_rename(self, old, new, merge=False):
- """rename serial_no text fields"""
- for dt in frappe.db.sql("""select parent from tabDocField
- where fieldname='serial_no' and fieldtype in ('Text', 'Small Text', 'Long Text')"""):
-
- for item in frappe.db.sql("""select name, serial_no from `tab%s`
- where serial_no like %s""" % (dt[0], frappe.db.escape('%' + old + '%'))):
-
- serial_nos = map(lambda i: new if i.upper()==old.upper() else i, item[1].split('\n'))
- frappe.db.sql("""update `tab%s` set serial_no = %s
- where name=%s""" % (dt[0], '%s', '%s'),
- ('\n'.join(list(serial_nos)), item[0]))
-
def update_serial_no_reference(self, serial_no=None):
last_sle = self.get_last_sle(serial_no)
self.set_purchase_details(last_sle.get("purchase_sle"))
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a00d63e..93e303c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -35,10 +35,16 @@
from erpnext.stock.utils import get_bin, get_incoming_rate
-class IncorrectValuationRateError(frappe.ValidationError): pass
-class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass
-class OperationsNotCompleteError(frappe.ValidationError): pass
-class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass
+class FinishedGoodError(frappe.ValidationError):
+ pass
+class IncorrectValuationRateError(frappe.ValidationError):
+ pass
+class DuplicateEntryForWorkOrderError(frappe.ValidationError):
+ pass
+class OperationsNotCompleteError(frappe.ValidationError):
+ pass
+class MaxSampleAlreadyRetainedError(frappe.ValidationError):
+ pass
from erpnext.controllers.stock_controller import StockController
@@ -701,6 +707,11 @@
finished_item = self.get_finished_item()
+ if not finished_item and self.purpose == "Manufacture":
+ # In case of independent Manufacture entry, don't auto set
+ # user must decide and set
+ return
+
for d in self.items:
if d.t_warehouse and not d.s_warehouse:
if self.purpose=="Repack" or d.item_code == finished_item:
@@ -721,38 +732,64 @@
return finished_item
def validate_finished_goods(self):
- """validation: finished good quantity should be same as manufacturing quantity"""
- if not self.work_order: return
+ """
+ 1. Check if FG exists
+ 2. Check if Multiple FG Items are present
+ 3. Check FG Item and Qty against WO if present
+ """
+ production_item, wo_qty, finished_items = None, 0, []
- production_item, wo_qty = frappe.db.get_value("Work Order",
- self.work_order, ["production_item", "qty"])
+ wo_details = frappe.db.get_value(
+ "Work Order", self.work_order, ["production_item", "qty"]
+ )
+ if wo_details:
+ production_item, wo_qty = wo_details
- finished_items = []
for d in self.get('items'):
if d.is_finished_item:
+ if not self.work_order:
+ finished_items.append(d.item_code)
+ continue # Independent Manufacture Entry, no WO to match against
+
if d.item_code != production_item:
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
- .format(d.item_code, self.work_order))
+ .format(d.item_code, self.work_order)
+ )
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
- frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
- format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}")
+ .format(d.idx, d.transfer_qty, self.fg_completed_qty)
+ )
+
finished_items.append(d.item_code)
if len(set(finished_items)) > 1:
- frappe.throw(_("Multiple items cannot be marked as finished item"))
+ frappe.throw(
+ msg=_("Multiple items cannot be marked as finished item"),
+ title=_("Note"),
+ exc=FinishedGoodError
+ )
if self.purpose == "Manufacture":
if not finished_items:
- frappe.throw(_('Finished Good has not set in the stock entry {0}')
- .format(self.name))
+ frappe.throw(
+ msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
+ title=_("Missing Finished Good"),
+ exc=FinishedGoodError
+ )
- allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
- "overproduction_percentage_for_work_order"))
+ allowance_percentage = flt(
+ frappe.db.get_single_value(
+ "Manufacturing Settings","overproduction_percentage_for_work_order"
+ )
+ )
+ allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty)
- allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty)
- if self.fg_completed_qty > allowed_qty:
- frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}")
- .format(flt(self.fg_completed_qty), wo_qty))
+ # No work order could mean independent Manufacture entry, if so skip validation
+ if self.work_order and self.fg_completed_qty > allowed_qty:
+ frappe.throw(
+ _("For quantity {0} should not be greater than work order quantity {1}")
+ .format(flt(self.fg_completed_qty), wo_qty)
+ )
def update_stock_ledger(self):
sl_entries = []
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 5a9e77e..b874874 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -15,7 +15,10 @@
set_item_variant_settings,
)
from erpnext.stock.doctype.serial_no.serial_no import * # noqa
-from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse
+from erpnext.stock.doctype.stock_entry.stock_entry import (
+ FinishedGoodError,
+ move_sample_to_retention_warehouse,
+)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@@ -929,6 +932,38 @@
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([40.0, 60.0], distributed_costs)
+ def test_independent_manufacture_entry(self):
+ "Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked."
+ se = frappe.get_doc(
+ doctype="Stock Entry",
+ purpose="Manufacture",
+ stock_entry_type="Manufacture",
+ company="_Test Company",
+ items=[
+ frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
+ frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC")
+ ]
+ )
+ # SE must have atleast one FG
+ self.assertRaises(FinishedGoodError, se.save)
+
+ se.items[0].is_finished_item = 1
+ se.items[1].is_finished_item = 1
+ # SE cannot have multiple FGs
+ self.assertRaises(FinishedGoodError, se.save)
+
+ se.items[0].is_finished_item = 0
+ se.save()
+
+ # Check if FG cost is calculated based on RM total cost
+ # RM total cost = 200, FG rate = 200/4(FG qty) = 50
+ self.assertEqual(se.items[1].basic_rate, 50)
+ self.assertEqual(se.value_difference, 0.0)
+ self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
+
+ # teardown
+ se.delete()
+
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty