Merge branch 'version-13-hotfix' into feat-bom-process-loss

This commit is contained in:
18alantom
2021-08-26 13:16:43 +05:30
77 changed files with 1547 additions and 261 deletions

View File

@@ -21,6 +21,10 @@ class BankTransaction(StatusUpdater):
self.update_allocations()
self.clear_linked_payment_entries()
self.set_status(update=True)
def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True)
def update_allocations(self):
if self.payment_entries:
@@ -41,21 +45,30 @@ class BankTransaction(StatusUpdater):
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
self.reload()
def clear_linked_payment_entries(self):
def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
self.clear_simple_entry(payment_entry)
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
self.clear_sales_invoice(payment_entry)
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
def clear_simple_entry(self, payment_entry):
frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date)
def clear_simple_entry(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
payment_entry.payment_document, payment_entry.payment_entry,
"clearance_date", clearance_date)
def clear_sales_invoice(self, payment_entry):
frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.payment_document,
parent=payment_entry.payment_entry), "clearance_date", self.date)
def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
"Sales Invoice Payment",
dict(
parenttype=payment_entry.payment_document,
parent=payment_entry.payment_entry
),
"clearance_date", clearance_date)
def get_total_allocated_amount(payment_entry):
return frappe.db.sql("""

View File

@@ -4,10 +4,12 @@
frappe.listview_settings['Bank Transaction'] = {
add_fields: ["unallocated_amount"],
get_indicator: function(doc) {
if(flt(doc.unallocated_amount)>0) {
return [__("Unreconciled"), "orange", "unallocated_amount,>,0"];
if(doc.docstatus == 2) {
return [__("Cancelled"), "red", "docstatus,=,2"];
} else if(flt(doc.unallocated_amount)<=0) {
return [__("Reconciled"), "green", "unallocated_amount,=,0"];
} else if(flt(doc.unallocated_amount)>0) {
return [__("Unreconciled"), "orange", "unallocated_amount,>,0"];
}
}
};

View File

@@ -25,7 +25,8 @@ class TestBankTransaction(unittest.TestCase):
def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel()
if doc.docstatus == 1:
doc.cancel()
doc.delete()
# Delete directly in DB to avoid validation errors for countries not allowing deletion
@@ -57,6 +58,12 @@ class TestBankTransaction(unittest.TestCase):
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertTrue(clearance_date is not None)
bank_transaction.reload()
bank_transaction.cancel()
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertFalse(clearance_date)
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))

View File

@@ -1564,7 +1564,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2021-08-18 16:13:52.080543",
"modified": "2021-08-24 18:19:20.728433",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -138,7 +138,7 @@ class POSInvoice(SalesInvoice):
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_stock_availablility(self):
if self.is_return:
if self.is_return or self.docstatus != 1:
return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')

View File

@@ -247,7 +247,7 @@
"depends_on": "customer",
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1,
@@ -692,10 +692,11 @@
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
"options": "Barcode",
"hide_days": 1,
"hide_seconds": 1,
"label": "Scan Barcode"
"label": "Scan Barcode",
"length": 1,
"options": "Barcode"
},
{
"allow_bulk_edit": 1,
@@ -1059,6 +1060,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Apply Additional Discount On",
"length": 15,
"options": "\nGrand Total\nNet Total",
"print_hide": 1
},
@@ -1145,7 +1147,7 @@
{
"description": "In Words will be visible once you save the Sales Invoice.",
"fieldname": "base_in_words",
"fieldtype": "Data",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words (Company Currency)",
@@ -1205,7 +1207,7 @@
},
{
"fieldname": "in_words",
"fieldtype": "Data",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words",
@@ -1558,6 +1560,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Print Language",
"length": 6,
"print_hide": 1,
"read_only": 1
},
@@ -1645,6 +1648,7 @@
"hide_seconds": 1,
"in_standard_filter": 1,
"label": "Status",
"length": 30,
"no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
@@ -1704,6 +1708,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Opening Entry",
"length": 4,
"oldfieldname": "is_opening",
"oldfieldtype": "Select",
"options": "No\nYes",
@@ -1715,6 +1720,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "C-Form Applicable",
"length": 4,
"no_copy": 1,
"options": "No\nYes",
"print_hide": 1
@@ -2017,7 +2023,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-08-18 16:07:45.122570",
"modified": "2021-08-25 14:46:05.279588",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -0,0 +1,34 @@
{
"actions": [],
"autoname": "account",
"creation": "2021-07-08 22:04:24.634967",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account"
],
"fields": [
{
"allow_in_quick_entry": 1,
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"in_preview": 1,
"label": "Account",
"options": "Account"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-08 22:35:33.202911",
"modified_by": "Administrator",
"module": "Accounts",
"name": "South Africa VAT Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SouthAfricaVATAccount(Document):
pass

View File

@@ -240,14 +240,15 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0
invoice_filters = {
'name': ('in', vouchers),
'docstatus': 1
'name': ('in', vouchers),
'docstatus': 1,
'apply_tds': 1
}
field = 'sum(net_total)'
if not cint(tax_details.consider_party_ledger_amount):
invoice_filters.update({'apply_tds': 1})
if cint(tax_details.consider_party_ledger_amount):
invoice_filters.pop('apply_tds', None)
field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0

View File

@@ -145,6 +145,36 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
def test_tds_calculation_on_net_total(self):
frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS")
invoices = []
pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True)
pi.append('taxes', {
"category": "Total",
"charge_type": "Actual",
"account_head": '_Test Account VAT - _TC',
"cost_center": 'Main - _TC',
"tax_amount": 1000,
"description": "Test",
"add_deduct_tax": "Add"
})
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000)
pi1.submit()
invoices.append(pi1)
self.assertEqual(pi1.taxes[0].tax_amount, 4000)
#delete invoices to avoid clashing
for d in invoices:
d.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
@@ -220,7 +250,7 @@ def create_sales_invoice(**args):
def create_records():
# create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']:
if frappe.db.exists('Supplier', name):
continue

View File

@@ -78,13 +78,10 @@ def validate_filters(filters, account_details):
def validate_party(filters):
party_type, party = filters.get("party_type"), filters.get("party")
if party:
if not party_type:
frappe.throw(_("To filter based on Party, select Party Type first"))
else:
for d in party:
if not frappe.db.exists(party_type, d):
frappe.throw(_("Invalid {0}: {1}").format(party_type, d))
if party and party_type:
for d in party:
if not frappe.db.exists(party_type, d):
frappe.throw(_("Invalid {0}: {1}").format(party_type, d))
def set_account_currency(filters):
if filters.get("account") or (filters.get('party') and len(filters.party) == 1):

View File

@@ -59,7 +59,7 @@ def make_depreciation_entry(asset_name, date=None):
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": ""
"cost_center": depreciation_cost_center
}
debit_entry = {

View File

@@ -164,7 +164,8 @@ class AccountsController(TransactionBase):
self.set_due_date()
self.set_payment_schedule()
self.validate_payment_schedule_amount()
self.validate_due_date()
if not self.get('ignore_default_payment_terms_template'):
self.validate_due_date()
self.validate_advance_entries()
def validate_non_invoice_documents_schedule(self):

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime
from frappe import _, throw
from frappe import _, bold, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor
@@ -16,7 +16,6 @@ from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency,
self.grand_total)
@@ -169,39 +168,96 @@ class SellingController(StockController):
def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field):
bold_net_rate = frappe.bold("net rate")
msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""")
.format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate)))
msg += "<br><br>"
msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""")
.format(get_link_to_form("Selling Settings", "Selling Settings")))
frappe.throw(msg, title=_("Invalid Selling Price"))
throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}.
Selling {3} should be atleast {4}.<br><br>Alternatively,
you can disable selling price validation in {5} to bypass
this validation.""").format(
idx,
bold(item_name),
bold(ref_rate_field),
bold("net rate"),
bold(rate),
get_link_to_form("Selling Settings", "Selling Settings"),
), title=_("Invalid Selling Price"))
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
return
if hasattr(self, "is_return") and self.is_return:
if (
self.get("is_return")
or not frappe.db.get_single_value("Selling Settings", "validate_selling_price")
):
return
for it in self.get("items"):
if not it.item_code:
is_internal_customer = self.get('is_internal_customer')
valuation_rate_map = {}
for item in self.items:
if not item.item_code:
continue
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1)
if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate")
last_purchase_rate, is_stock_item = frappe.get_cached_value(
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
)
last_valuation_rate = frappe.db.sql("""
SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s
AND warehouse = %s AND valuation_rate > 0
ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1
""", (it.item_code, it.warehouse))
if last_valuation_rate:
last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1)
if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \
and not self.get('is_internal_customer'):
throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")
last_purchase_rate_in_sales_uom = (
last_purchase_rate * (item.conversion_factor or 1)
)
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
throw_message(
item.idx,
item.item_name,
last_purchase_rate_in_sales_uom,
"last purchase rate"
)
if is_internal_customer or not is_stock_item:
continue
valuation_rate_map[(item.item_code, item.warehouse)] = None
if not valuation_rate_map:
return
or_conditions = (
f"""(item_code = {frappe.db.escape(valuation_rate[0])}
and warehouse = {frappe.db.escape(valuation_rate[1])})"""
for valuation_rate in valuation_rate_map
)
valuation_rates = frappe.db.sql(f"""
select
item_code, warehouse, valuation_rate
from
`tabBin`
where
({" or ".join(or_conditions)})
and valuation_rate > 0
""", as_dict=True)
for rate in valuation_rates:
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
for item in self.items:
if not item.item_code:
continue
last_valuation_rate = valuation_rate_map.get(
(item.item_code, item.warehouse)
)
if not last_valuation_rate:
continue
last_valuation_rate_in_sales_uom = (
last_valuation_rate * (item.conversion_factor or 1)
)
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
throw_message(
item.idx,
item.item_name,
last_valuation_rate_in_sales_uom,
"valuation rate"
)
def get_item_list(self):
il = []

View File

@@ -86,7 +86,8 @@ status_map = {
],
"Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],
["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"]
["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"],
["Cancelled", "eval:self.docstatus == 2"]
],
"POS Opening Entry": [
["Draft", None],

View File

@@ -95,9 +95,17 @@ frappe.ui.form.on("Opportunity", {
}, __('Create'));
}
frm.add_custom_button(__('Quotation'),
cur_frm.cscript.create_quotation, __('Create'));
if (frm.doc.opportunity_from != "Customer") {
frm.add_custom_button(__('Customer'),
function() {
frm.trigger("make_customer")
}, __('Create'));
}
frm.add_custom_button(__('Quotation'),
function() {
frm.trigger("create_quotation")
}, __('Create'));
}
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {
@@ -194,6 +202,13 @@ erpnext.crm.Opportunity = frappe.ui.form.Controller.extend({
method: "erpnext.crm.doctype.opportunity.opportunity.make_quotation",
frm: cur_frm
})
},
make_customer: function() {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.opportunity.opportunity.make_customer",
frm: cur_frm
})
}
});

View File

@@ -287,6 +287,24 @@ def make_request_for_quotation(source_name, target_doc=None):
return doclist
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
def set_missing_values(source, target):
if source.opportunity_from == "Lead":
target.lead_name = source.party_name
doclist = get_mapped_doc("Opportunity", source_name, {
"Opportunity": {
"doctype": "Customer",
"field_map": {
"currency": "default_currency",
"customer_name": "customer_name"
}
}
}, target_doc, set_missing_values)
return doclist
@frappe.whitelist()
def make_supplier_quotation(source_name, target_doc=None):
doclist = get_mapped_doc("Opportunity", source_name, {

View File

@@ -29,10 +29,10 @@ class TestFeeValidity(unittest.TestCase):
healthcare_settings.save(ignore_permissions=True)
patient, practitioner = create_healthcare_docs()
# appointment should not be invoiced. Check Fee Validity created for new patient
# For first appointment, invoice is generated
appointment = create_appointment(patient, practitioner, nowdate())
invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced")
self.assertEqual(invoiced, 0)
self.assertEqual(invoiced, 1)
# appointment should not be invoiced as it is within fee validity
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4))

View File

@@ -282,7 +282,7 @@
],
"image_field": "image",
"links": [],
"modified": "2021-01-22 10:14:43.187675",
"modified": "2021-08-24 10:42:08.513054",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Practitioner",
@@ -295,6 +295,7 @@
"read": 1,
"report": 1,
"role": "Laboratory User",
"select": 1,
"share": 1,
"write": 1
},
@@ -307,6 +308,7 @@
"read": 1,
"report": 1,
"role": "Physician",
"select": 1,
"share": 1,
"write": 1
},
@@ -319,6 +321,7 @@
"read": 1,
"report": 1,
"role": "Nursing User",
"select": 1,
"share": 1,
"write": 1
}

View File

@@ -241,6 +241,13 @@ frappe.ui.form.on('Patient Appointment', {
frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0);
frm.toggle_reqd('billing_item', 0);
} else if (data.message) {
frm.toggle_display('mode_of_payment', 1);
frm.toggle_display('paid_amount', 1);
frm.toggle_display('billing_item', 1);
frm.toggle_reqd('mode_of_payment', 1);
frm.toggle_reqd('paid_amount', 1);
frm.toggle_reqd('billing_item', 1);
} else {
// if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0);

View File

@@ -134,6 +134,7 @@
"read_only": 1
},
{
"depends_on": "eval:doc.practitioner;",
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Appointment Details"
@@ -141,7 +142,6 @@
{
"fieldname": "practitioner",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_standard_filter": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
@@ -400,4 +400,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -163,8 +163,6 @@ def check_payment_fields_reqd(patient):
fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'})
if fee_validity:
return {'fee_validity': fee_validity}
if check_is_new_patient(patient):
return False
return True
return False
@@ -179,8 +177,6 @@ def invoice_appointment(appointment_doc):
elif not fee_validity:
if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}):
return
if check_is_new_patient(appointment_doc.patient, appointment_doc.name):
return
else:
fee_validity = None
@@ -224,9 +220,7 @@ def check_is_new_patient(patient, name=None):
filters['name'] = ('!=', name)
has_previous_appointment = frappe.db.exists('Patient Appointment', filters)
if has_previous_appointment:
return False
return True
return not has_previous_appointment
def get_appointment_item(appointment_doc, item):

View File

@@ -4,11 +4,12 @@
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter, check_payment_fields_reqd, check_is_new_patient
from frappe.utils import nowdate, add_days, now_datetime
from frappe.utils.make_random import get_random
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientAppointment(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabPatient Appointment`""")
@@ -179,6 +180,28 @@ class TestPatientAppointment(unittest.TestCase):
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1, now_datetime())
def test_payment_should_be_mandatory_for_new_patient_appointment(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
frappe.db.set_value('Healthcare Settings', None, 'max_visits', 3)
frappe.db.set_value('Healthcare Settings', None, 'valid_days', 30)
patient = create_patient()
assert check_is_new_patient(patient)
payment_required = check_payment_fields_reqd(patient)
assert payment_required is True
def test_sales_invoice_should_be_generated_for_new_patient_appointment(self):
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
invoice_count = frappe.db.count('Sales Invoice')
assert check_is_new_patient(patient)
create_appointment(patient, practitioner, nowdate())
new_invoice_count = frappe.db.count('Sales Invoice')
assert new_invoice_count == invoice_count + 1
def test_overlap_appointment(self):
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError
patient, practitioner = create_healthcare_docs(id=1)
@@ -228,6 +251,27 @@ class TestPatientAppointment(unittest.TestCase):
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0)
self.assertRaises(MaximumCapacityError, appointment.save)
def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self):
patient, practitioner = create_healthcare_docs()
create_appointment(patient, practitioner, nowdate())
patient, new_practitioner = create_healthcare_docs(id=2)
create_appointment(patient, new_practitioner, nowdate())
roles = [{"doctype": "Has Role", "role": "Physician"}]
user = create_user(roles=roles)
new_practitioner = frappe.get_doc('Healthcare Practitioner', new_practitioner)
new_practitioner.user_id = user.email
new_practitioner.save()
frappe.set_user(user.name)
appointments = frappe.get_list('Patient Appointment')
assert len(appointments) == 1
frappe.set_user("Administrator")
appointments = frappe.get_list('Patient Appointment')
assert len(appointments) == 2
def create_healthcare_docs(id=0):
patient = create_patient(id)
@@ -275,7 +319,6 @@ def create_practitioner(id=0, medical_department=None):
return practitioner.name
def create_encounter(appointment):
if appointment:
encounter = frappe.new_doc('Patient Encounter')
@@ -290,7 +333,6 @@ def create_encounter(appointment):
return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
@@ -400,3 +442,17 @@ def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0):
service_unit.save(ignore_permissions=True)
return service_unit.name
def create_user(email=None, roles=None):
if not email:
email = '{}@frappe.com'.format(frappe.utils.random_string(10))
user = frappe.db.exists('User', email)
if not user:
user = frappe.get_doc({
"doctype": "User",
"email": email,
"first_name": "test_user",
"password": "password",
"roles": roles,
}).insert()
return user

View File

@@ -243,7 +243,7 @@ doc_events = {
"on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions",
"erpnext.portal.utils.set_default_role"]
},
("Sales Taxes and Charges Template", 'Price List'): {
"Sales Taxes and Charges Template": {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
},
"Website Settings": {
@@ -444,6 +444,7 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'

View File

@@ -148,6 +148,7 @@ class BOM(WebsiteGenerator):
self.set_plc_conversion_rate()
self.validate_uom_is_interger()
self.set_bom_material_details()
self.set_bom_scrap_items_detail()
self.validate_materials()
self.set_routing_operations()
self.validate_operations()
@@ -202,7 +203,7 @@ class BOM(WebsiteGenerator):
def set_bom_material_details(self):
for item in self.get("items"):
self.validate_bom_currecny(item)
self.validate_bom_currency(item)
ret = self.get_bom_material_detail({
"company": self.company,
@@ -221,6 +222,19 @@ class BOM(WebsiteGenerator):
if not item.get(r):
item.set(r, ret[r])
def set_bom_scrap_items_detail(self):
for item in self.get("scrap_items"):
args = {
"item_code": item.item_code,
"company": self.company,
"scrap_items": True,
"bom_no": '',
}
ret = self.get_bom_material_detail(args)
for key, value in ret.items():
if not item.get(key):
item.set(key, value)
@frappe.whitelist()
def get_bom_material_detail(self, args=None):
""" Get raw material details like uom, desc and rate"""
@@ -257,7 +271,7 @@ class BOM(WebsiteGenerator):
return ret_item
def validate_bom_currecny(self, item):
def validate_bom_currency(self, item):
if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency:
frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}")
.format(item.idx, item.bom_no, self.currency))

View File

@@ -26,17 +26,17 @@ class JobCard(Document):
self.set_status()
self.validate_operation_id()
self.validate_sequence_id()
self.get_sub_operations()
self.set_sub_operations()
self.update_sub_operation_status()
def get_sub_operations(self):
def set_sub_operations(self):
if self.operation:
self.sub_operations = []
for row in frappe.get_all("Sub Operation",
filters = {"parent": self.operation}, fields=["operation", "idx"]):
row.status = "Pending"
for row in frappe.get_all('Sub Operation',
filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
row.status = 'Pending'
row.sub_operation = row.operation
self.append("sub_operations", row)
self.append('sub_operations', row)
def validate_time_logs(self):
self.total_time_in_mins = 0.0
@@ -690,7 +690,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
target.set('time_logs', [])
target.set('employee', [])
target.set('items', [])
target.get_sub_operations()
target.set_sub_operations()
target.get_required_items()
target.validate_time_logs()

View File

@@ -301,4 +301,5 @@ erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16
erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record

View File

@@ -0,0 +1,14 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from erpnext.regional.south_africa.setup import make_custom_fields, add_permissions
def execute():
company = frappe.get_all('Company', filters = {'country': 'South Africa'})
if not company:
return
make_custom_fields()
add_permissions()

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('South Africa VAT Settings', {
refresh: function(frm) {
frm.set_query("company", function() {
return {
filters: {
country: "South Africa",
}
};
});
frm.set_query("account", "vat_accounts", function() {
return {
filters: {
company: frm.doc.company,
account_type: "Tax",
is_group: 0
}
};
});
}
});

View File

@@ -0,0 +1,76 @@
{
"actions": [],
"autoname": "field:company",
"creation": "2021-07-08 22:34:33.668015",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"vat_accounts"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1,
"unique": 1
},
{
"fieldname": "vat_accounts",
"fieldtype": "Table",
"label": "VAT Accounts",
"options": "South Africa VAT Account",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-14 02:17:52.476762",
"modified_by": "Administrator",
"module": "Regional",
"name": "South Africa VAT Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Auditor",
"share": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SouthAfricaVATSettings(Document):
pass

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestSouthAfricaVATSettings(unittest.TestCase):
pass

View File

@@ -33,6 +33,14 @@ def get_datev_csv(data, filters, csv_class):
if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS:
result['Belegdatum'] = pd.to_datetime(result['Belegdatum'])
result['Beleginfo - Inhalt 6'] = pd.to_datetime(result['Beleginfo - Inhalt 6'])
result['Beleginfo - Inhalt 6'] = result['Beleginfo - Inhalt 6'].dt.strftime('%d%m%Y')
result['Fälligkeit'] = pd.to_datetime(result['Fälligkeit'])
result['Fälligkeit'] = result['Fälligkeit'].dt.strftime('%d%m%y')
result.sort_values(by='Belegdatum', inplace=True, kind='stable', ignore_index=True)
if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES:
result['Sprach-ID'] = 'de-DE'

View File

@@ -43,6 +43,12 @@ COLUMNS = [
"fieldtype": "Data",
"width": 100
},
{
"label": "BU-Schlüssel",
"fieldname": "BU-Schlüssel",
"fieldtype": "Data",
"width": 100
},
{
"label": "Belegdatum",
"fieldname": "Belegdatum",
@@ -114,6 +120,36 @@ COLUMNS = [
"fieldname": "Beleginfo - Inhalt 4",
"fieldtype": "Data",
"width": 150
},
{
"label": "Beleginfo - Art 5",
"fieldname": "Beleginfo - Art 5",
"fieldtype": "Data",
"width": 150
},
{
"label": "Beleginfo - Inhalt 5",
"fieldname": "Beleginfo - Inhalt 5",
"fieldtype": "Data",
"width": 100
},
{
"label": "Beleginfo - Art 6",
"fieldname": "Beleginfo - Art 6",
"fieldtype": "Data",
"width": 150
},
{
"label": "Beleginfo - Inhalt 6",
"fieldname": "Beleginfo - Inhalt 6",
"fieldtype": "Date",
"width": 100
},
{
"label": "Fälligkeit",
"fieldname": "Fälligkeit",
"fieldtype": "Date",
"width": 100
}
]
@@ -161,6 +197,125 @@ def validate_fiscal_year(from_date, to_date, company):
def get_transactions(filters, as_dict=1):
def run(params_method, filters):
extra_fields, extra_joins, extra_filters = params_method(filters)
return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict)
def sort_by(row):
# "Belegdatum" is in the fifth column when list format is used
return row["Belegdatum" if as_dict else 5]
type_map = {
# specific query methods for some voucher types
"Payment Entry": get_payment_entry_params,
"Sales Invoice": get_sales_invoice_params,
"Purchase Invoice": get_purchase_invoice_params
}
only_voucher_type = filters.get("voucher_type")
transactions = []
for voucher_type, get_voucher_params in type_map.items():
if only_voucher_type and only_voucher_type != voucher_type:
continue
transactions.extend(run(params_method=get_voucher_params, filters=filters))
if not only_voucher_type or only_voucher_type not in type_map:
# generic query method for all other voucher types
filters["exclude_voucher_types"] = type_map.keys()
transactions.extend(run(params_method=get_generic_params, filters=filters))
return sorted(transactions, key=sort_by)
def get_payment_entry_params(filters):
extra_fields = """
, 'Zahlungsreferenz' as 'Beleginfo - Art 5'
, pe.reference_no as 'Beleginfo - Inhalt 5'
, 'Buchungstag' as 'Beleginfo - Art 6'
, pe.reference_date as 'Beleginfo - Inhalt 6'
, '' as 'Fälligkeit'
"""
extra_joins = """
LEFT JOIN `tabPayment Entry` pe
ON gl.voucher_no = pe.name
"""
extra_filters = """
AND gl.voucher_type = 'Payment Entry'
"""
return extra_fields, extra_joins, extra_filters
def get_sales_invoice_params(filters):
extra_fields = """
, '' as 'Beleginfo - Art 5'
, '' as 'Beleginfo - Inhalt 5'
, '' as 'Beleginfo - Art 6'
, '' as 'Beleginfo - Inhalt 6'
, si.due_date as 'Fälligkeit'
"""
extra_joins = """
LEFT JOIN `tabSales Invoice` si
ON gl.voucher_no = si.name
"""
extra_filters = """
AND gl.voucher_type = 'Sales Invoice'
"""
return extra_fields, extra_joins, extra_filters
def get_purchase_invoice_params(filters):
extra_fields = """
, 'Lieferanten-Rechnungsnummer' as 'Beleginfo - Art 5'
, pi.bill_no as 'Beleginfo - Inhalt 5'
, 'Lieferanten-Rechnungsdatum' as 'Beleginfo - Art 6'
, pi.bill_date as 'Beleginfo - Inhalt 6'
, pi.due_date as 'Fälligkeit'
"""
extra_joins = """
LEFT JOIN `tabPurchase Invoice` pi
ON gl.voucher_no = pi.name
"""
extra_filters = """
AND gl.voucher_type = 'Purchase Invoice'
"""
return extra_fields, extra_joins, extra_filters
def get_generic_params(filters):
# produce empty fields so all rows will have the same length
extra_fields = """
, '' as 'Beleginfo - Art 5'
, '' as 'Beleginfo - Inhalt 5'
, '' as 'Beleginfo - Art 6'
, '' as 'Beleginfo - Inhalt 6'
, '' as 'Fälligkeit'
"""
extra_joins = ""
if filters.get("exclude_voucher_types"):
# exclude voucher types that are queried by a dedicated method
exclude = "({})".format(', '.join("'{}'".format(key) for key in filters.get("exclude_voucher_types")))
extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude)
# if voucher type filter is set, allow only this type
if filters.get("voucher_type"):
extra_filters += " AND gl.voucher_type = %(voucher_type)s"
return extra_fields, extra_joins, extra_filters
def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1):
"""
Get a list of accounting entries.
@@ -171,8 +326,7 @@ def get_transactions(filters, as_dict=1):
filters -- dict of filters to be passed to the sql query
as_dict -- return as list of dicts [0,1]
"""
filter_by_voucher = 'AND gl.voucher_type = %(voucher_type)s' if filters.get('voucher_type') else ''
gl_entries = frappe.db.sql("""
query = """
SELECT
/* either debit or credit amount; always positive */
@@ -187,6 +341,9 @@ def get_transactions(filters, as_dict=1):
/* against number or, if empty, party against number */
%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
/* disable automatic VAT deduction */
'40' as 'BU-Schlüssel',
gl.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1',
LEFT(gl.remarks, 60) as 'Buchungstext',
@@ -199,30 +356,34 @@ def get_transactions(filters, as_dict=1):
case gl.party_type when 'Customer' then 'Debitorennummer' when 'Supplier' then 'Kreditorennummer' else NULL end as 'Beleginfo - Art 4',
par.debtor_creditor_number as 'Beleginfo - Inhalt 4'
{extra_fields}
FROM `tabGL Entry` gl
/* Kontonummer */
left join `tabAccount` acc
on gl.account = acc.name
LEFT JOIN `tabAccount` acc
ON gl.account = acc.name
left join `tabCustomer` cus
on gl.party_type = 'Customer'
and gl.party = cus.name
LEFT JOIN `tabParty Account` par
ON par.parent = gl.party
AND par.parenttype = gl.party_type
AND par.company = %(company)s
left join `tabSupplier` sup
on gl.party_type = 'Supplier'
and gl.party = sup.name
left join `tabParty Account` par
on par.parent = gl.party
and par.parenttype = gl.party_type
and par.company = %(company)s
{extra_joins}
WHERE gl.company = %(company)s
AND DATE(gl.posting_date) >= %(from_date)s
AND DATE(gl.posting_date) <= %(to_date)s
{}
ORDER BY 'Belegdatum', gl.voucher_no""".format(filter_by_voucher), filters, as_dict=as_dict)
{extra_filters}
ORDER BY 'Belegdatum', gl.voucher_no""".format(
extra_fields=extra_fields,
extra_joins=extra_joins,
extra_filters=extra_filters
)
gl_entries = frappe.db.sql(query, filters, as_dict=as_dict)
return gl_entries

View File

@@ -588,7 +588,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.2.9",
gst_json = {"version": "GST3.0.4",
"hash": "hash", "gstin": gstin, "fp": fp}
res = {}
@@ -765,7 +765,7 @@ def get_cdnr_reg_json(res, gstin):
"ntty": invoice[0]["document_type"],
"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
"rchrg": invoice[0]["reverse_charge"],
"inv_type": get_invoice_type_for_cdnr(invoice[0])
"inv_typ": get_invoice_type_for_cdnr(invoice[0])
}
inv_item["itms"] = []

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["VAT Audit Report"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -2),
"width": "80"
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.get_today()
}
]
};

View File

@@ -0,0 +1,22 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-07-09 11:07:43.473518",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-07-09 11:07:43.473518",
"modified_by": "Administrator",
"module": "Regional",
"name": "VAT Audit Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "VAT Audit Report",
"report_type": "Script Report",
"roles": []
}

View File

@@ -0,0 +1,269 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.utils import formatdate
def execute(filters=None):
return VATAuditReport(filters).run()
class VATAuditReport(object):
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.columns = []
self.data = []
self.doctypes = ["Purchase Invoice", "Sales Invoice"]
def run(self):
self.get_sa_vat_accounts()
self.get_columns()
for doctype in self.doctypes:
self.select_columns = """
name as voucher_no,
posting_date, remarks"""
columns = ", supplier as party, credit_to as account" if doctype=="Purchase Invoice" \
else ", customer as party, debit_to as account"
self.select_columns += columns
self.get_invoice_data(doctype)
if self.invoices:
self.get_invoice_items(doctype)
self.get_items_based_on_tax_rate(doctype)
self.get_data(doctype)
return self.columns, self.data
def get_sa_vat_accounts(self):
self.sa_vat_accounts = frappe.get_list("South Africa VAT Account",
filters = {"parent": self.filters.company}, pluck="account")
if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate:
frappe.throw(_("Please set VAT Accounts in South Africa VAT Settings"))
def get_invoice_data(self, doctype):
conditions = self.get_conditions()
self.invoices = frappe._dict()
invoice_data = frappe.db.sql("""
SELECT
{select_columns}
FROM
`tab{doctype}`
WHERE
docstatus = 1 {where_conditions}
and is_opening = "No"
ORDER BY
posting_date DESC
""".format(select_columns=self.select_columns, doctype=doctype,
where_conditions=conditions), self.filters, as_dict=1)
for d in invoice_data:
self.invoices.setdefault(d.voucher_no, d)
def get_invoice_items(self, doctype):
self.invoice_items = frappe._dict()
items = frappe.db.sql("""
SELECT
item_code, parent, taxable_value, base_net_amount, is_zero_rated
FROM
`tab%s Item`
WHERE
parent in (%s)
""" % (doctype, ", ".join(["%s"]*len(self.invoices))), tuple(self.invoices), as_dict=1)
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {
'net_amount': 0.0})
self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated
def get_items_based_on_tax_rate(self, doctype):
self.items_based_on_tax_rate = frappe._dict()
self.item_tax_rate = frappe._dict()
self.tax_doctype = "Purchase Taxes and Charges" if doctype=="Purchase Invoice" \
else "Sales Taxes and Charges"
self.tax_details = frappe.db.sql("""
SELECT
parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount
FROM
`tab%s`
WHERE
parenttype = %s and docstatus = 1
and parent in (%s)
ORDER BY
account_head
""" % (self.tax_doctype, "%s", ", ".join(["%s"]*len(self.invoices.keys()))),
tuple([doctype] + list(self.invoices.keys())))
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
if item_wise_tax_detail:
try:
if account in self.sa_vat_accounts:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
else:
continue
for item_code, taxes in item_wise_tax_detail.items():
is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated")
#to skip items with non-zero tax rate in multiple rows
if taxes[0] == 0 and not is_zero_rated:
continue
tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes)
if tax_rate is not None:
rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}) \
.setdefault(tax_rate, [])
if item_code not in rate_based_dict:
rate_based_dict.append(item_code)
except ValueError:
continue
def get_item_amount_map(self, parent, item_code, taxes):
net_amount = self.invoice_items.get(parent).get(item_code).get("net_amount")
tax_rate = taxes[0]
tax_amount = taxes[1]
gross_amount = net_amount + tax_amount
item_amount_map = self.item_tax_rate.setdefault(parent, {}) \
.setdefault(item_code, [])
amount_dict = {
"tax_rate": tax_rate,
"gross_amount": gross_amount,
"tax_amount": tax_amount,
"net_amount": net_amount
}
item_amount_map.append(amount_dict)
return tax_rate, item_amount_map
def get_conditions(self):
conditions = ""
for opts in (("company", " and company=%(company)s"),
("from_date", " and posting_date>=%(from_date)s"),
("to_date", " and posting_date<=%(to_date)s")):
if self.filters.get(opts[0]):
conditions += opts[1]
return conditions
def get_data(self, doctype):
consolidated_data = self.get_consolidated_data(doctype)
section_name = _("Purchases") if doctype == "Purchase Invoice" else _("Sales")
for rate, section in consolidated_data.items():
rate = int(rate)
label = frappe.bold(section_name + "- " + "Rate" + " " + str(rate) + "%")
section_head = {"posting_date": label}
total_gross = total_tax = total_net = 0
self.data.append(section_head)
for row in section.get("data"):
self.data.append(row)
total_gross += row["gross_amount"]
total_tax += row["tax_amount"]
total_net += row["net_amount"]
total = {
"posting_date": frappe.bold(_("Total")),
"gross_amount": total_gross,
"tax_amount": total_tax,
"net_amount": total_net,
"bold":1
}
self.data.append(total)
self.data.append({})
def get_consolidated_data(self, doctype):
consolidated_data_map={}
for inv, inv_data in self.invoices.items():
if self.items_based_on_tax_rate.get(inv):
for rate, items in self.items_based_on_tax_rate.get(inv).items():
consolidated_data_map.setdefault(rate, {"data": []})
for item in items:
row = {}
item_details = self.item_tax_rate.get(inv).get(item)
row["account"] = inv_data.get("account")
row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy")
row["voucher_type"] = doctype
row["voucher_no"] = inv
row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier"
row["party"] = inv_data.get("party")
row["remarks"] = inv_data.get("remarks")
row["gross_amount"]= item_details[0].get("gross_amount")
row["tax_amount"]= item_details[0].get("tax_amount")
row["net_amount"]= item_details[0].get("net_amount")
consolidated_data_map[rate]["data"].append(row)
return consolidated_data_map
def get_columns(self):
self.columns = [
{
"fieldname": "posting_date",
"label": "Posting Date",
"fieldtype": "Data",
"width": 200
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"options": "Account",
"width": 150
},
{
"fieldname": "voucher_type",
"label": "Voucher Type",
"fieldtype": "Data",
"width": 140,
"hidden": 1
},
{
"fieldname": "voucher_no",
"label": "Reference",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 150
},
{
"fieldname": "party_type",
"label": "Party Type",
"fieldtype": "Data",
"width": 140,
"hidden": 1
},
{
"fieldname": "party",
"label": "Party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 150
},
{
"fieldname": "remarks",
"label": "Details",
"fieldtype": "Data",
"width": 150
},
{
"fieldname": "net_amount",
"label": "Net Amount",
"fieldtype": "Currency",
"width": 130
},
{
"fieldname": "tax_amount",
"label": "Tax Amount",
"fieldtype": "Currency",
"width": 130
},
{
"fieldname": "gross_amount",
"label": "Gross Amount",
"fieldtype": "Currency",
"width": 130
},
]

View File

@@ -0,0 +1,50 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property
def setup(company=None, patch=True):
make_custom_fields()
add_permissions()
def make_custom_fields(update=True):
is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated',
fieldtype='Check', fetch_from='item_code.is_zero_rated',
insert_after='description', print_hide=1)
custom_fields = {
'Item': [
dict(fieldname='is_zero_rated', label='Is Zero Rated',
fieldtype='Check', insert_after='item_group',
print_hide=1)
],
'Sales Invoice Item': is_zero_rated,
'Purchase Invoice Item': is_zero_rated
}
create_custom_fields(custom_fields, update=update)
def add_permissions():
"""Add Permissions for South Africa VAT Settings and South Africa VAT Account
and VAT Audit Report"""
for doctype in ('South Africa VAT Settings', 'South Africa VAT Account'):
add_permission(doctype, 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission(doctype, role, 0)
update_permission_property(doctype, role, 0, 'write', 1)
update_permission_property(doctype, role, 0, 'create', 1)
if not frappe.db.get_value('Custom Role', dict(report="VAT Audit Report")):
frappe.get_doc(dict(
doctype='Custom Role',
report="VAT Audit Report",
roles= [
dict(role='Accounts User'),
dict(role='Accounts Manager'),
dict(role='Auditor')
]
)).insert()

View File

@@ -111,7 +111,6 @@ frappe.ui.form.on("Customer", {
}
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Customer'}
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
if(!frm.doc.__islocal) {
frappe.contacts.render_address_and_contact(frm);

View File

@@ -20,6 +20,7 @@
"tax_withholding_category",
"default_bank_account",
"lead_name",
"opportunity_name",
"image",
"column_break0",
"account_manager",
@@ -267,6 +268,7 @@
"options": "fa fa-map-marker"
},
{
"depends_on": "eval: !doc.__islocal",
"fieldname": "address_html",
"fieldtype": "HTML",
"label": "Address HTML",
@@ -283,6 +285,7 @@
"width": "50%"
},
{
"depends_on": "eval: !doc.__islocal",
"fieldname": "contact_html",
"fieldtype": "HTML",
"label": "Contact HTML",
@@ -493,6 +496,14 @@
"fieldtype": "Link",
"label": "Tax Withholding Category",
"options": "Tax Withholding Category"
},
{
"fieldname": "opportunity_name",
"fieldtype": "Link",
"label": "From Opportunity",
"no_copy": 1,
"options": "Opportunity",
"print_hide": 1
}
],
"icon": "fa fa-user",
@@ -500,7 +511,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-01-28 12:54:57.258959",
"modified": "2021-08-25 18:56:09.929905",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -390,6 +390,10 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
},
_set_batch_number: function(doc) {
if (doc.batch_no) {
return
}
let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)};
if (doc.has_serial_no && doc.serial_no) {
args['serial_no'] = doc.serial_no

View File

@@ -63,11 +63,11 @@ class TestCurrencyExchange(unittest.TestCase):
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
self.assertEqual(exchange_rate, 62.9)
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io
# Exchange rate as on 15th Dec, 2015
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
self.assertFalse(exchange_rate == 60)
self.assertEqual(flt(exchange_rate, 3), 66.894)
self.assertEqual(flt(exchange_rate, 3), 66.999)
def test_exchange_rate_strict(self):
# strict currency settings
@@ -77,28 +77,17 @@ class TestCurrencyExchange(unittest.TestCase):
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying")
self.assertEqual(exchange_rate, 60.0)
# Will fetch from fixer.io
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 67.79)
self.assertEqual(flt(exchange_rate, 3), 67.235)
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
self.assertEqual(exchange_rate, 62.9)
# Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io
# Exchange rate as on 15th Dec, 2015
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 66.894)
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10", "for_selling")
self.assertEqual(exchange_rate, 65.1)
# NGN is not available on fixer.io so these should return 0
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09", "for_selling")
self.assertEqual(exchange_rate, 0)
exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11", "for_selling")
self.assertEqual(exchange_rate, 0)
self.assertEqual(flt(exchange_rate, 3), 66.999)
def test_exchange_rate_strict_switched(self):
# Start with allow_stale is True
@@ -111,4 +100,4 @@ class TestCurrencyExchange(unittest.TestCase):
# Will fetch from fixer.io
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
self.assertEqual(flt(exchange_rate, 3), 67.79)
self.assertEqual(flt(exchange_rate, 3), 67.235)

View File

@@ -93,21 +93,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
try:
cache = frappe.cache()
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date,from_currency, to_currency)
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)
value = cache.get(key)
if not value:
import requests
api_url = "https://frankfurter.app/{0}".format(transaction_date)
api_url = "https://api.exchangerate.host/convert"
response = requests.get(api_url, params={
"base": from_currency,
"symbols": to_currency
"date": transaction_date,
"from": from_currency,
"to": to_currency
})
# expire in 6 hours
response.raise_for_status()
value = response.json()["rates"][to_currency]
cache.set_value(key, value, expires_in_sec=6 * 60 * 60)
value = response.json()["result"]
cache.setex(name=key, time=21600, value=flt(value))
return flt(value)
except:
frappe.log_error(title="Get Exchange Rate")

View File

@@ -6,7 +6,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _, msgprint
from frappe.utils import comma_and
from frappe.utils import flt
from frappe.model.document import Document
from frappe.utils import get_datetime, get_datetime_str, now_datetime
@@ -18,46 +18,35 @@ class ShoppingCartSettings(Document):
def validate(self):
if self.enabled:
self.validate_exchange_rates_exist()
self.validate_price_list_exchange_rate()
def validate_price_list_exchange_rate(self):
"Check if exchange rate exists for Price List currency (to Company's currency)."
from erpnext.setup.utils import get_exchange_rate
if not self.enabled or not self.company or not self.price_list:
return # this function is also called from hooks, check values again
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency")
def validate_exchange_rates_exist(self):
"""check if exchange rates exist for all Price List currencies (to company's currency)"""
company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
if not company_currency:
msgprint(_("Please specify currency in Company") + ": " + self.company,
raise_exception=ShoppingCartSetupError)
msg = f"Please specify currency in Company {self.company}"
frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
price_list_currency_map = frappe.db.get_values("Price List",
[self.price_list], "currency")
if not price_list_currency:
msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}"
frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
price_list_currency_map = dict(price_list_currency_map)
if price_list_currency != company_currency:
from_currency, to_currency = price_list_currency, company_currency
# check if all price lists have a currency
for price_list, currency in price_list_currency_map.items():
if not currency:
frappe.throw(_("Currency is required for Price List {0}").format(price_list))
# Get exchange rate checks Currency Exchange Records too
exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling")
expected_to_exist = [currency + "-" + company_currency
for currency in price_list_currency_map.values()
if currency != company_currency]
# manqala 20/09/2016: set up selection parameters for query from tabCurrency Exchange
from_currency = [currency for currency in price_list_currency_map.values() if currency != company_currency]
to_currency = company_currency
# manqala end
if expected_to_exist:
# manqala 20/09/2016: modify query so that it uses date in the selection from Currency Exchange.
# exchange rates defined with date less than the date on which this document is being saved will be selected
exists = frappe.db.sql_list("""select CONCAT(from_currency,'-',to_currency) from `tabCurrency Exchange`
where from_currency in (%s) and to_currency = "%s" and date <= curdate()""" % (", ".join(["%s"]*len(from_currency)), to_currency), tuple(from_currency))
# manqala end
missing = list(set(expected_to_exist).difference(exists))
if missing:
msgprint(_("Missing Currency Exchange Rates for {0}").format(comma_and(missing)),
raise_exception=ShoppingCartSetupError)
if not flt(exchange_rate):
msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}"
frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError)
def validate_tax_rule(self):
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"):
@@ -71,7 +60,7 @@ class ShoppingCartSettings(Document):
def get_shipping_rules(self, shipping_territory):
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
def validate_cart_settings(doc, method):
def validate_cart_settings(doc=None, method=None):
frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate")
def get_shopping_cart_settings():

View File

@@ -16,17 +16,25 @@ class TestShoppingCartSettings(unittest.TestCase):
return frappe.get_doc({"doctype": "Shopping Cart Settings",
"company": "_Test Company"})
def test_exchange_rate_exists(self):
frappe.db.sql("""delete from `tabCurrency Exchange`""")
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
# We aren't checking just currency exchange record anymore
# while validating price list currency exchange rate to that of company.
# The API is being used to fetch the rate which again almost always
# gives back a valid value (for valid currencies).
# This makes the test obsolete.
# Commenting because im not sure if there's a better test we can write
cart_settings = self.get_cart_settings()
cart_settings.price_list = "_Test Price List Rest of the World"
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist)
# def test_exchange_rate_exists(self):
# frappe.db.sql("""delete from `tabCurrency Exchange`""")
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
currency_exchange_records
frappe.get_doc(currency_exchange_records[0]).insert()
cart_settings.validate_exchange_rates_exist()
# cart_settings = self.get_cart_settings()
# cart_settings.price_list = "_Test Price List Rest of the World"
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
# currency_exchange_records
# frappe.get_doc(currency_exchange_records[0]).insert()
# cart_settings.validate_price_list_exchange_rate()
def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")

View File

@@ -356,3 +356,23 @@ erpnext.stock.delivery_note.set_print_hide = function(doc, cdt, cdn){
dn_fields['taxes'].print_hide = 0;
}
}
frappe.tour['Delivery Note'] = [
{
fieldname: "customer",
title: __("Customer"),
description: __("This field is used to set the 'Customer'.")
},
{
fieldname: "items",
title: __("Items"),
description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " +
__("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.")
},
{
fieldname: "set_posting_time",
title: __("Edit Posting Date and Time"),
description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.")
}
]

View File

@@ -792,4 +792,4 @@ frappe.ui.form.on("UOM Conversion Detail", {
});
}
}
})
});

View File

@@ -13,6 +13,9 @@ class PriceList(Document):
if not cint(self.buying) and not cint(self.selling):
throw(_("Price List must be applicable for Buying or Selling"))
if not self.is_new():
self.check_impact_on_shopping_cart()
def on_update(self):
self.set_default_if_missing()
self.update_item_price()
@@ -32,6 +35,17 @@ class PriceList(Document):
buying=%s, selling=%s, modified=NOW() where price_list=%s""",
(self.currency, cint(self.buying), cint(self.selling), self.name))
def check_impact_on_shopping_cart(self):
"Check if Price List currency change impacts Shopping Cart."
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import validate_cart_settings
doc_before_save = self.get_doc_before_save()
currency_changed = self.currency != doc_before_save.currency
affects_cart = self.name == frappe.get_cached_value("Shopping Cart Settings", None, "price_list")
if currency_changed and affects_cart:
validate_cart_settings()
def on_trash(self):
self.delete_price_list_details_key()

View File

@@ -1101,3 +1101,4 @@ function check_should_not_attach_bom_items(bom_no) {
}
$.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));

View File

@@ -288,3 +288,4 @@ erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({
});
cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm});

View File

@@ -16,36 +16,3 @@ frappe.ui.form.on('Stock Settings', {
}
});
frappe.tour['Stock Settings'] = [
{
fieldname: "item_naming_by",
title: __("Item Naming By"),
description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a>" + __(" choose the 'Naming Series' option."),
},
{
fieldname: "default_warehouse",
title: __("Default Warehouse"),
description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.")
},
{
fieldname: "allow_negative_stock",
title: __("Allow Negative Stock"),
description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.")
},
{
fieldname: "valuation_method",
title: __("Valuation Method"),
description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average' target='_blank'>here</a>" + __(" to know more about them.")
},
{
fieldname: "show_barcode_field",
title: __("Show Barcode Field"),
description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.")
},
{
fieldname: "automatically_set_serial_nos_based_on_fifo",
title: __("Automatically Set Serial Nos based on FIFO"),
description: __("Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.")
}
];

View File

@@ -86,3 +86,4 @@ function convert_to_group_or_ledger(frm){
})
}

View File

@@ -0,0 +1,56 @@
{
"creation": "2021-08-24 14:44:22.292652",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-25 16:31:31.441194",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
"owner": "Administrator",
"reference_doctype": "Stock Entry",
"save_on_complete": 1,
"steps": [
{
"description": "Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/stock/articles/stock-entry-purpose#2purpose-material-receipt\" target=\"_blank\">Material Receipt.</a>",
"field": "",
"fieldname": "stock_entry_type",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Stock Entry Type",
"next_step_condition": "eval: doc.stock_entry_type === \"Material Receipt\"",
"parent_field": "",
"position": "Top",
"title": "Stock Entry Type"
},
{
"description": "Select a target warehouse where the stock will be received.",
"field": "",
"fieldname": "to_warehouse",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Default Target Warehouse",
"next_step_condition": "eval: doc.to_warehouse",
"parent_field": "",
"position": "Top",
"title": "Default Target Warehouse"
},
{
"description": "Select an item and entry quantity to be delivered.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Items",
"next_step_condition": "eval: doc.items[0]?.item_code",
"parent_field": "",
"position": "Top",
"title": "Items"
}
],
"title": "Stock Entry"
}

View File

@@ -0,0 +1,55 @@
{
"creation": "2021-08-24 14:44:46.770952",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-25 16:26:11.718664",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
"owner": "Administrator",
"reference_doctype": "Stock Reconciliation",
"save_on_complete": 1,
"steps": [
{
"description": "Set Purpose to Opening Stock to set the stock opening balance.",
"field": "",
"fieldname": "purpose",
"fieldtype": "Select",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Purpose",
"next_step_condition": "eval: doc.purpose === \"Opening Stock\"",
"parent_field": "",
"position": "Top",
"title": "Purpose"
},
{
"description": "Select the items for which the opening stock has to be set.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Items",
"next_step_condition": "eval: doc.items[0]?.item_code",
"parent_field": "",
"position": "Top",
"title": "Items"
},
{
"description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.",
"field": "",
"fieldname": "posting_date",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Posting Date",
"parent_field": "",
"position": "Bottom",
"title": "Posting Date"
}
],
"title": "Stock Reconciliation"
}

View File

@@ -0,0 +1,89 @@
{
"creation": "2021-08-20 15:20:59.336585",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-25 16:19:37.699528",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
"owner": "Administrator",
"reference_doctype": "Stock Settings",
"save_on_complete": 1,
"steps": [
{
"description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.",
"field": "",
"fieldname": "item_naming_by",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Item Naming By",
"parent_field": "",
"position": "Bottom",
"title": "Item Naming By"
},
{
"description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.",
"field": "",
"fieldname": "default_warehouse",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Warehouse",
"parent_field": "",
"position": "Bottom",
"title": "Default Warehouse"
},
{
"description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.",
"field": "",
"fieldname": "action_if_quality_inspection_is_not_submitted",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Action If Quality Inspection Is Not Submitted",
"parent_field": "",
"position": "Bottom",
"title": "Action if Quality Inspection Is Not Submitted"
},
{
"description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.",
"field": "",
"fieldname": "automatically_set_serial_nos_based_on_fifo",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Automatically Set Serial Nos Based on FIFO",
"parent_field": "",
"position": "Bottom",
"title": "Automatically Set Serial Nos based on FIFO"
},
{
"description": "Show 'Scan Barcode' field above every child table to insert Items with ease.",
"field": "",
"fieldname": "show_barcode_field",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Show Barcode Field in Stock Transactions",
"parent_field": "",
"position": "Bottom",
"title": "Show Barcode Field"
},
{
"description": "Choose between FIFO and Moving Average Valuation Methods. Click <a href=\"https://docs.erpnext.com/docs/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average\" target=\"_blank\">here</a> to know more about them.",
"field": "",
"fieldname": "valuation_method",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Valuation Method",
"parent_field": "",
"position": "Bottom",
"title": "Default Valuation Method"
}
],
"title": "Stock Settings"
}

View File

@@ -0,0 +1,54 @@
{
"creation": "2021-08-24 14:43:44.465237",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-24 14:50:31.988256",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
"owner": "Administrator",
"reference_doctype": "Warehouse",
"save_on_complete": 1,
"steps": [
{
"description": "Select a name for the warehouse. This should reflect its location or purpose.",
"field": "",
"fieldname": "warehouse_name",
"fieldtype": "Data",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Warehouse Name",
"next_step_condition": "eval: doc.warehouse_name",
"parent_field": "",
"position": "Bottom",
"title": "Warehouse Name"
},
{
"description": "Select a warehouse type to categorize the warehouse into a sub-group.",
"field": "",
"fieldname": "warehouse_type",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Warehouse Type",
"parent_field": "",
"position": "Top",
"title": "Warehouse Type"
},
{
"description": "Select an account to set a default account for all transactions with this warehouse.",
"field": "",
"fieldname": "account",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Account",
"parent_field": "",
"position": "Top",
"title": "Account"
}
],
"title": "Warehouse"
}

View File

@@ -278,6 +278,10 @@ def get_basic_details(args, item, overwrite_warehouse=True):
else:
args.uom = item.stock_uom
if (args.get("batch_no") and
item.name != frappe.get_cached_value('Batch', args.get("batch_no"), 'item')):
args['batch_no'] = ''
out = frappe._dict({
"item_code": item.name,
"item_name": item.item_name,

View File

@@ -19,32 +19,26 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock",
"idx": 0,
"is_complete": 0,
"modified": "2020-10-14 14:54:42.741971",
"modified": "2021-08-20 14:38:55.570067",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
"owner": "Administrator",
"steps": [
{
"step": "Setup your Warehouse"
"step": "Stock Settings"
},
{
"step": "Create a Product"
},
{
"step": "Create a Supplier"
},
{
"step": "Introduction to Stock Entry"
"step": "Create a Warehouse"
},
{
"step": "Create a Stock Entry"
},
{
"step": "Create a Purchase Receipt"
"step": "Stock Opening Balance"
},
{
"step": "Stock Settings"
"step": "View Stock Projected Qty"
}
],
"subtitle": "Inventory, Warehouses, Analysis, and more.",

View File

@@ -1,19 +0,0 @@
{
"action": "Create Entry",
"creation": "2020-05-19 18:59:13.266713",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-14 14:53:25.618434",
"modified_by": "Administrator",
"name": "Create a Purchase Receipt",
"owner": "Administrator",
"reference_document": "Purchase Receipt",
"show_full_form": 1,
"title": "Create a Purchase Receipt",
"validate_action": 1
}

View File

@@ -1,19 +1,21 @@
{
"action": "Create Entry",
"action_label": "Create a Material Transfer Entry",
"creation": "2020-05-15 03:20:16.277043",
"description": "# Manage Stock Movements\nStock entry allows you to register the movement of stock for various purposes like transfer, received, issues, repacked, etc. To address issues related to theft and pilferages, you can always ensure that the movement of goods happens against a document reference Stock Entry in ERPNext.\n\nLet\u2019s get a quick walk-through on the various scenarios covered in Stock Entry by watching [*this video*](https://www.youtube.com/watch?v=Njt107hlY3I).",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-14 14:53:00.105905",
"modified": "2021-06-18 13:57:11.434063",
"modified_by": "Administrator",
"name": "Create a Stock Entry",
"owner": "Administrator",
"reference_document": "Stock Entry",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Create a Stock Entry",
"title": "Manage Stock Movements",
"validate_action": 1
}

View File

@@ -1,18 +1,19 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"creation": "2020-05-14 22:09:10.043554",
"description": "# Create a Supplier\nIn this step we will create a **Supplier**. If you have already created a **Supplier** you can skip this step.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-14 14:53:00.120455",
"modified": "2021-05-17 16:37:37.697077",
"modified_by": "Administrator",
"name": "Create a Supplier",
"owner": "Administrator",
"reference_document": "Supplier",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create a Supplier",
"validate_action": 1

View File

@@ -0,0 +1,21 @@
{
"action": "Create Entry",
"action_label": "Let\u2019s create your first warehouse ",
"creation": "2021-05-17 16:13:19.297789",
"description": "# Setup a Warehouse\nThe warehouse can be your location/godown/store where you maintain the item's inventory, and receive/deliver them to various parties.\n\nIn ERPNext, you can maintain a Warehouse in the tree structure, so that location and sub-location of an item can be tracked. Also, you can link a Warehouse to a specific Accounting ledger, where the real-time stock value of that warehouse\u2019s item will be reflected.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-18 12:23:36.675572",
"modified_by": "Administrator",
"name": "Create a Warehouse",
"owner": "Administrator",
"reference_document": "Warehouse",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Setup a Warehouse",
"validate_action": 1
}

View File

@@ -0,0 +1,22 @@
{
"action": "Create Entry",
"action_label": "",
"creation": "2021-05-17 13:47:18.515052",
"description": "# Create an Item\nThe Stock module deals with the movement of items.\n\nIn this step we will create an [**Item**](https://docs.erpnext.com/docs/user/manual/en/stock/item).",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"intro_video_url": "",
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-05-18 16:15:20.695028",
"modified_by": "Administrator",
"name": "Create an Item",
"owner": "Administrator",
"reference_document": "Item",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Create an Item",
"validate_action": 1
}

View File

@@ -1,17 +1,18 @@
{
"action": "Watch Video",
"creation": "2020-05-15 02:47:17.958806",
"description": "# Introduction to Stock Entry\nThis video will give a quick introduction to [**Stock Entry**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-entry).",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-14 14:53:00.075177",
"modified": "2021-05-18 15:13:43.306064",
"modified_by": "Administrator",
"name": "Introduction to Stock Entry",
"owner": "Administrator",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Introduction to Stock Entry",
"validate_action": 1,

View File

@@ -5,15 +5,15 @@
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-14 14:53:25.538900",
"modified": "2021-05-17 13:53:06.936579",
"modified_by": "Administrator",
"name": "Setup your Warehouse",
"owner": "Administrator",
"path": "Tree/Warehouse",
"reference_document": "Warehouse",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Set up your Warehouse",
"validate_action": 1

View File

@@ -0,0 +1,22 @@
{
"action": "Create Entry",
"action_label": "Let\u2019s create a stock opening entry",
"creation": "2021-05-17 16:13:47.511883",
"description": "# Update Stock Opening Balance\nIt\u2019s an entry to update the stock balance of an item, in a warehouse, on a date and time you are going live on ERPNext.\n\nOnce opening stocks are updated, you can create transactions like manufacturing and stock deliveries, where this opening stock will be consumed.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-06-18 13:59:36.021097",
"modified_by": "Administrator",
"name": "Stock Opening Balance",
"owner": "Administrator",
"reference_document": "Stock Reconciliation",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Update Stock Opening Balance",
"validate_action": 1,
"video_url": "https://www.youtube.com/watch?v=nlHX0ZZ84Lw"
}

View File

@@ -1,19 +1,21 @@
{
"action": "Show Form Tour",
"action_label": "Take a walk through Stock Settings",
"creation": "2020-05-15 02:53:57.209967",
"description": "# Review Stock Settings\n\nIn ERPNext, the Stock module\u2019s features are configurable as per your business needs. Stock Settings is the place where you can set your preferences for:\n- Default values for Item and Pricing\n- Default valuation method for inventory valuation\n- Set preference for serialization and batching of item\n- Set tolerance for over-receipt and delivery of items",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2020-10-14 14:53:00.092504",
"modified": "2021-08-18 12:06:51.139387",
"modified_by": "Administrator",
"name": "Stock Settings",
"owner": "Administrator",
"reference_document": "Stock Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Explore Stock Settings",
"title": "Review Stock Settings",
"validate_action": 1
}

View File

@@ -0,0 +1,24 @@
{
"action": "View Report",
"action_label": "Check Stock Projected Qty",
"creation": "2021-08-20 14:38:41.649103",
"description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-20 14:38:41.649103",
"modified_by": "Administrator",
"name": "View Stock Projected Qty",
"owner": "Administrator",
"reference_report": "Stock Projected Qty",
"report_description": "You can set the filters to narrow the results, then click on Generate New Report to see the updated report.",
"report_reference_doctype": "Item",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Check Stock Projected Qty",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "Go to Page",
"creation": "2021-05-17 16:12:43.427579",
"description": "# View Warehouse\nIn ERPNext the term 'warehouse' can be thought of as a storage location.\n\nWarehouses are arranged in ERPNext in a tree like structure, where multiple sub-warehouses can be grouped under a single warehouse.\n\nIn this step we will view the [**Warehouse Tree**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse#21-tree-view) to view the [**Warehouses**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse) that are set by default.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-05-18 15:04:41.198413",
"modified_by": "Administrator",
"name": "View Warehouses",
"owner": "Administrator",
"path": "Tree/Warehouse",
"show_form_tour": 0,
"show_full_form": 0,
"title": "View Warehouses",
"validate_action": 1
}

View File

@@ -23,6 +23,7 @@ def execute(filters=None):
conversion_factors = []
if opening_row:
data.append(opening_row)
conversion_factors.append(0)
actual_qty = stock_value = 0

View File

@@ -1,4 +1,4 @@
{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %}
{% from "templates/includes/rfq/rfq_macros.html" import item_name_and_description %}
{% for d in doc.items %}
<div class="rfq-item">

View File

@@ -86,7 +86,7 @@
<span class="small gray">{{d.transaction_date}}</span>
</div>
</div>
<a class="transaction-item-link" href="/quotations/{{d.name}}">Link</a>
<a class="transaction-item-link" href="/supplier-quotations/{{d.name}}">Link</a>
</div>
{% endfor %}
</div>
@@ -95,6 +95,4 @@
</div>
</div>
</div>
{% endblock %}