Merge branch 'develop' into crm-carry-forward-communication-comments
This commit is contained in:
commit
2fd870ff15
@ -218,6 +218,8 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
|||||||
# updated clear date of all the vouchers based on the bank transaction
|
# updated clear date of all the vouchers based on the bank transaction
|
||||||
vouchers = json.loads(vouchers)
|
vouchers = json.loads(vouchers)
|
||||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
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:
|
if transaction.unallocated_amount == 0:
|
||||||
frappe.throw(_("This bank transaction is already fully reconciled"))
|
frappe.throw(_("This bank transaction is already fully reconciled"))
|
||||||
total_amount = 0
|
total_amount = 0
|
||||||
@ -226,7 +228,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
|||||||
total_amount += get_paid_amount(frappe._dict({
|
total_amount += get_paid_amount(frappe._dict({
|
||||||
'payment_document': voucher['payment_doctype'],
|
'payment_document': voucher['payment_doctype'],
|
||||||
'payment_entry': voucher['payment_name'],
|
'payment_entry': voucher['payment_name'],
|
||||||
}), transaction.currency)
|
}), transaction.currency, company_account)
|
||||||
|
|
||||||
if total_amount > transaction.unallocated_amount:
|
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"))
|
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 @@ def get_linked_payments(bank_transaction_name, document_types = None):
|
|||||||
return matching
|
return matching
|
||||||
|
|
||||||
def check_matching(bank_account, company, transaction, document_types):
|
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)
|
subquery = get_queries(bank_account, company, transaction, document_types)
|
||||||
filters = {
|
filters = {
|
||||||
"amount": transaction.unallocated_amount,
|
"amount": transaction.unallocated_amount,
|
||||||
@ -343,13 +345,11 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
|
|||||||
def get_je_matching_query(amount_condition, transaction):
|
def get_je_matching_query(amount_condition, transaction):
|
||||||
# get matching journal entry query
|
# 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")
|
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
|
||||||
root_type = frappe.get_value("Account", company_account, "root_type")
|
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
|
|
||||||
|
|||||||
@ -102,7 +102,7 @@ def get_total_allocated_amount(payment_entry):
|
|||||||
AND
|
AND
|
||||||
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
|
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"]:
|
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||||
|
|
||||||
paid_amount_field = "paid_amount"
|
paid_amount_field = "paid_amount"
|
||||||
@ -115,7 +115,7 @@ def get_paid_amount(payment_entry, currency):
|
|||||||
payment_entry.payment_entry, paid_amount_field)
|
payment_entry.payment_entry, paid_amount_field)
|
||||||
|
|
||||||
elif payment_entry.payment_document == "Journal Entry":
|
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":
|
elif payment_entry.payment_document == "Expense Claim":
|
||||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
|
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
|
||||||
|
|||||||
@ -986,7 +986,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
|
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
|
||||||
pi.set_posting_time = 1
|
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].enable_deferred_expense = 1
|
||||||
pi.items[0].service_start_date = "2019-01-10"
|
pi.items[0].service_start_date = "2019-01-10"
|
||||||
pi.items[0].service_end_date = "2019-03-15"
|
pi.items[0].service_end_date = "2019-03-15"
|
||||||
|
|||||||
@ -409,7 +409,7 @@ def get_plan_from_razorpay_id(plan_id):
|
|||||||
def set_expired_status():
|
def set_expired_status():
|
||||||
frappe.db.sql("""
|
frappe.db.sql("""
|
||||||
UPDATE
|
UPDATE
|
||||||
`tabMembership` SET `status` = 'Expired'
|
`tabMembership` SET `membership_status` = 'Expired'
|
||||||
WHERE
|
WHERE
|
||||||
`status` not in ('Cancelled') AND `to_date` < %s
|
`membership_status` not in ('Cancelled') AND `to_date` < %s
|
||||||
""", (nowdate()))
|
""", (nowdate()))
|
||||||
|
|||||||
@ -171,6 +171,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
salary_slip.end_date = month_end_date
|
salary_slip.end_date = month_end_date
|
||||||
salary_slip.save()
|
salary_slip.save()
|
||||||
salary_slip.submit()
|
salary_slip.submit()
|
||||||
|
salary_slip.reload()
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = self.get_no_of_days()
|
||||||
days_in_month = no_of_days[0]
|
days_in_month = no_of_days[0]
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class TestProjectProfitability(unittest.TestCase):
|
|||||||
|
|
||||||
self.timesheet = make_timesheet(emp, is_billable=1)
|
self.timesheet = make_timesheet(emp, is_billable=1)
|
||||||
self.salary_slip = make_salary_slip(self.timesheet.name)
|
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)
|
holidays = self.salary_slip.get_holidays_for_employee(date, date)
|
||||||
if holidays:
|
if holidays:
|
||||||
@ -41,8 +42,8 @@ class TestProjectProfitability(unittest.TestCase):
|
|||||||
def test_project_profitability(self):
|
def test_project_profitability(self):
|
||||||
filters = {
|
filters = {
|
||||||
'company': '_Test Company',
|
'company': '_Test Company',
|
||||||
'start_date': add_days(getdate(), -3),
|
'start_date': add_days(self.timesheet.start_date, -3),
|
||||||
'end_date': getdate()
|
'end_date': self.timesheet.start_date
|
||||||
}
|
}
|
||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
|
||||||
"autoname": "field:serial_no",
|
"autoname": "field:serial_no",
|
||||||
"creation": "2013-05-16 10:59:15",
|
"creation": "2013-05-16 10:59:15",
|
||||||
"description": "Distinct unit of an Item",
|
"description": "Distinct unit of an Item",
|
||||||
@ -434,10 +433,11 @@
|
|||||||
"icon": "fa fa-barcode",
|
"icon": "fa fa-barcode",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-01-08 14:31:15.375996",
|
"modified": "2021-12-23 10:44:30.299450",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Serial No",
|
"name": "Serial No",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -476,5 +476,6 @@
|
|||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@ -194,23 +194,6 @@ class SerialNo(StockController):
|
|||||||
if sle_exists:
|
if sle_exists:
|
||||||
frappe.throw(_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name))
|
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):
|
def update_serial_no_reference(self, serial_no=None):
|
||||||
last_sle = self.get_last_sle(serial_no)
|
last_sle = self.get_last_sle(serial_no)
|
||||||
self.set_purchase_details(last_sle.get("purchase_sle"))
|
self.set_purchase_details(last_sle.get("purchase_sle"))
|
||||||
|
|||||||
@ -35,10 +35,16 @@ from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get
|
|||||||
from erpnext.stock.utils import get_bin, get_incoming_rate
|
from erpnext.stock.utils import get_bin, get_incoming_rate
|
||||||
|
|
||||||
|
|
||||||
class IncorrectValuationRateError(frappe.ValidationError): pass
|
class FinishedGoodError(frappe.ValidationError):
|
||||||
class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass
|
pass
|
||||||
class OperationsNotCompleteError(frappe.ValidationError): pass
|
class IncorrectValuationRateError(frappe.ValidationError):
|
||||||
class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass
|
pass
|
||||||
|
class DuplicateEntryForWorkOrderError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class OperationsNotCompleteError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class MaxSampleAlreadyRetainedError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
|
|
||||||
@ -701,6 +707,11 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
finished_item = self.get_finished_item()
|
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:
|
for d in self.items:
|
||||||
if d.t_warehouse and not d.s_warehouse:
|
if d.t_warehouse and not d.s_warehouse:
|
||||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||||
@ -721,38 +732,64 @@ class StockEntry(StockController):
|
|||||||
return finished_item
|
return finished_item
|
||||||
|
|
||||||
def validate_finished_goods(self):
|
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",
|
wo_details = frappe.db.get_value(
|
||||||
self.work_order, ["production_item", "qty"])
|
"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'):
|
for d in self.get('items'):
|
||||||
if d.is_finished_item:
|
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:
|
if d.item_code != production_item:
|
||||||
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
|
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):
|
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
|
||||||
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
|
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}")
|
||||||
format(d.idx, d.transfer_qty, self.fg_completed_qty))
|
.format(d.idx, d.transfer_qty, self.fg_completed_qty)
|
||||||
|
)
|
||||||
|
|
||||||
finished_items.append(d.item_code)
|
finished_items.append(d.item_code)
|
||||||
|
|
||||||
if len(set(finished_items)) > 1:
|
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 self.purpose == "Manufacture":
|
||||||
if not finished_items:
|
if not finished_items:
|
||||||
frappe.throw(_('Finished Good has not set in the stock entry {0}')
|
frappe.throw(
|
||||||
.format(self.name))
|
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",
|
allowance_percentage = flt(
|
||||||
"overproduction_percentage_for_work_order"))
|
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)
|
# No work order could mean independent Manufacture entry, if so skip validation
|
||||||
if self.fg_completed_qty > allowed_qty:
|
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}")
|
frappe.throw(
|
||||||
.format(flt(self.fg_completed_qty), wo_qty))
|
_("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):
|
def update_stock_ledger(self):
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
|
|||||||
@ -15,7 +15,10 @@ from erpnext.stock.doctype.item.test_item import (
|
|||||||
set_item_variant_settings,
|
set_item_variant_settings,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import * # noqa
|
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_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_ledger_entry.stock_ledger_entry import StockFreezeError
|
||||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||||
@ -929,6 +932,38 @@ class TestStockEntry(ERPNextTestCase):
|
|||||||
distributed_costs = [d.additional_cost for d in se.items]
|
distributed_costs = [d.additional_cost for d in se.items]
|
||||||
self.assertEqual([40.0, 60.0], distributed_costs)
|
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})
|
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||||
def test_future_negative_sle(self):
|
def test_future_negative_sle(self):
|
||||||
# Initialize item, batch, warehouse, opening qty
|
# Initialize item, batch, warehouse, opening qty
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user